aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet/App/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet/App/Controller')
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm1109
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Bodies.pm476
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm1
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm16
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm6
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Users.pm685
-rw-r--r--perllib/FixMyStreet/App/Controller/Around.pm36
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm24
-rw-r--r--perllib/FixMyStreet/App/Controller/Contact.pm45
-rw-r--r--perllib/FixMyStreet/App/Controller/Council.pm13
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm207
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Develop.pm127
-rwxr-xr-xperllib/FixMyStreet/App/Controller/JS.pm4
-rw-r--r--perllib/FixMyStreet/App/Controller/JSON.pm23
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm406
-rw-r--r--perllib/FixMyStreet/App/Controller/My.pm7
-rw-r--r--perllib/FixMyStreet/App/Controller/Open311.pm9
-rw-r--r--perllib/FixMyStreet/App/Controller/Photo.pm22
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Questionnaire.pm10
-rw-r--r--perllib/FixMyStreet/App/Controller/Report.pm242
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm285
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm13
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm171
-rw-r--r--perllib/FixMyStreet/App/Controller/Root.pm1
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Rss.pm4
-rw-r--r--perllib/FixMyStreet/App/Controller/Tokens.pm26
26 files changed, 2311 insertions, 1657 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index 7d04f5ff9..2f4669456 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -13,6 +13,7 @@ use List::Util 'first';
use List::MoreUtils 'uniq';
use mySociety::ArrayUtils;
use Text::CSV;
+use Try::Tiny;
use FixMyStreet::SendReport;
use FixMyStreet::SMS;
@@ -142,353 +143,51 @@ sub timeline : Path( 'timeline' ) : Args(0) {
my %time;
- $c->model('DB')->schema->storage->sql_maker->quote_char( '"' );
- $c->model('DB')->schema->storage->sql_maker->name_sep( '.' );
+ try {
+ $c->model('DB')->schema->storage->sql_maker->quote_char( '"' );
+ $c->model('DB')->schema->storage->sql_maker->name_sep( '.' );
- my $probs = $c->cobrand->problems->timeline;
+ my $probs = $c->cobrand->problems->timeline;
- foreach ($probs->all) {
- push @{$time{$_->created->epoch}}, { type => 'problemCreated', date => $_->created, obj => $_ };
- push @{$time{$_->confirmed->epoch}}, { type => 'problemConfirmed', date => $_->confirmed, obj => $_ } if $_->confirmed;
- push @{$time{$_->whensent->epoch}}, { type => 'problemSent', date => $_->whensent, obj => $_ } if $_->whensent;
- }
-
- my $questionnaires = $c->model('DB::Questionnaire')->timeline( $c->cobrand->restriction );
-
- foreach ($questionnaires->all) {
- push @{$time{$_->whensent->epoch}}, { type => 'quesSent', date => $_->whensent, obj => $_ };
- push @{$time{$_->whenanswered->epoch}}, { type => 'quesAnswered', date => $_->whenanswered, obj => $_ } if $_->whenanswered;
- }
-
- my $updates = $c->cobrand->updates->timeline;
-
- foreach ($updates->all) {
- push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created, obj => $_} ;
- }
-
- my $alerts = $c->model('DB::Alert')->timeline_created( $c->cobrand->restriction );
-
- foreach ($alerts->all) {
- push @{$time{$_->whensubscribed->epoch}}, { type => 'alertSub', date => $_->whensubscribed, obj => $_ };
- }
-
- $alerts = $c->model('DB::Alert')->timeline_disabled( $c->cobrand->restriction );
-
- foreach ($alerts->all) {
- push @{$time{$_->whendisabled->epoch}}, { type => 'alertDel', date => $_->whendisabled, obj => $_ };
- }
-
- $c->model('DB')->schema->storage->sql_maker->quote_char( '' );
-
- $c->stash->{time} = \%time;
-
- return 1;
-}
-
-sub bodies : Path('bodies') : Args(0) {
- my ( $self, $c ) = @_;
-
- if (my $body_id = $c->get_param('body')) {
- return $c->res->redirect( $c->uri_for( 'body', $body_id ) );
- }
-
- if (!$c->user->is_superuser && $c->user->from_body && $c->cobrand->moniker ne 'zurich') {
- return $c->res->redirect( $c->uri_for( 'body', $c->user->from_body->id ) );
- }
-
- $c->forward( '/auth/get_csrf_token' );
-
- my $edit_activity = $c->model('DB::ContactsHistory')->search(
- undef,
- {
- select => [ 'editor', { count => 'contacts_history_id', -as => 'c' } ],
- as => [ 'editor', 'c' ],
- group_by => ['editor'],
- order_by => { -desc => 'c' }
+ foreach ($probs->all) {
+ push @{$time{$_->created->epoch}}, { type => 'problemCreated', date => $_->created, obj => $_ };
+ push @{$time{$_->confirmed->epoch}}, { type => 'problemConfirmed', date => $_->confirmed, obj => $_ } if $_->confirmed;
+ push @{$time{$_->whensent->epoch}}, { type => 'problemSent', date => $_->whensent, obj => $_ } if $_->whensent;
}
- );
-
- $c->stash->{edit_activity} = $edit_activity;
- $c->forward( 'fetch_languages' );
- $c->forward( 'fetch_translations' );
+ my $questionnaires = $c->model('DB::Questionnaire')->timeline( $c->cobrand->restriction );
- my $posted = $c->get_param('posted') || '';
- if ( $posted eq 'body' ) {
- $c->forward('check_for_super_user');
- $c->forward('/auth/check_csrf_token');
-
- my $values = $c->forward('body_params');
- unless ( keys %{$c->stash->{body_errors}} ) {
- 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 => $_ } );
- }
-
- $c->stash->{object} = $body;
- $c->stash->{translation_col} = 'name';
- $c->forward('update_translations');
- $c->stash->{updated} = _('New body added');
+ foreach ($questionnaires->all) {
+ push @{$time{$_->whensent->epoch}}, { type => 'quesSent', date => $_->whensent, obj => $_ };
+ push @{$time{$_->whenanswered->epoch}}, { type => 'quesAnswered', date => $_->whenanswered, obj => $_ } if $_->whenanswered;
}
- }
- $c->forward( 'fetch_all_bodies' );
+ my $updates = $c->cobrand->updates->timeline;
- my $contacts = $c->model('DB::Contact')->search(
- undef,
- {
- select => [ 'body_id', { count => 'id' }, { count => \'case when state = \'deleted\' then 1 else null end' },
- { count => \'case when state = \'confirmed\' then 1 else null end' } ],
- as => [qw/body_id c deleted confirmed/],
- group_by => [ 'body_id' ],
- result_class => 'DBIx::Class::ResultClass::HashRefInflator'
+ foreach ($updates->all) {
+ push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created, obj => $_} ;
}
- );
-
- my %council_info = map { $_->{body_id} => $_ } $contacts->all;
-
- $c->stash->{counts} = \%council_info;
-
- $c->forward( 'body_form_dropdowns' );
-
- return 1;
-}
-
-sub body_form_dropdowns : Private {
- my ( $self, $c ) = @_;
-
- my $areas;
- my $whitelist = $c->config->{MAPIT_ID_WHITELIST};
-
- if ( $whitelist && ref $whitelist eq 'ARRAY' && @$whitelist ) {
- $areas = mySociety::MaPit::call('areas', $whitelist);
- } 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:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders };
- $c->stash->{send_methods} = \@methods;
-}
-
-sub check_for_super_user : Private {
- my ( $self, $c ) = @_;
-
- my $superuser = $c->user->is_superuser;
- # Zurich currently has its own way of defining superusers
- $superuser ||= $c->cobrand->moniker eq 'zurich' && $c->stash->{admin_type} eq 'super';
-
- unless ( $superuser ) {
- $c->detach('/page_error_403_access_denied', []);
- }
-}
-sub update_contacts : Private {
- my ( $self, $c ) = @_;
-
- my $posted = $c->get_param('posted');
- my $editor = $c->forward('get_user');
-
- if ( $posted eq 'new' ) {
- $c->forward('/auth/check_csrf_token');
-
- my %errors;
-
- my $category = $self->trim( $c->get_param('category') );
- $errors{category} = _("Please choose a category") unless $category;
- $errors{note} = _('Please enter a message') unless $c->get_param('note');
-
- my $contact = $c->model('DB::Contact')->find_or_new(
- {
- body_id => $c->stash->{body_id},
- category => $category,
- }
- );
-
- 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_list($email) || $email eq 'REFUSED';
- }
+ my $alerts = $c->model('DB::Alert')->timeline_created( $c->cobrand->restriction );
- $contact->email( $email );
- $contact->state( $c->get_param('state') );
- $contact->non_public( $c->get_param('non_public') ? 1 : 0 );
- $contact->note( $c->get_param('note') );
- $contact->whenedited( \'current_timestamp' );
- $contact->editor( $editor );
- $contact->endpoint( $c->get_param('endpoint') );
- $contact->jurisdiction( $c->get_param('jurisdiction') );
- $contact->api_key( $c->get_param('api_key') );
- $contact->send_method( $c->get_param('send_method') );
-
- # Set flags in extra to the appropriate values
- if ( $c->get_param('photo_required') ) {
- $contact->set_extra_metadata_if_undefined( photo_required => 1 );
- }
- else {
- $contact->unset_extra_metadata( 'photo_required' );
- }
- if ( $c->get_param('inspection_required') ) {
- $contact->set_extra_metadata( inspection_required => 1 );
- }
- else {
- $contact->unset_extra_metadata( 'inspection_required' );
- }
- if ( $c->get_param('reputation_threshold') ) {
- $contact->set_extra_metadata( reputation_threshold => int($c->get_param('reputation_threshold')) );
+ foreach ($alerts->all) {
+ push @{$time{$_->whensubscribed->epoch}}, { type => 'alertSub', date => $_->whensubscribed, obj => $_ };
}
- $c->forward('update_extra_fields', [ $contact ]);
- $c->forward('contact_cobrand_extra_fields', [ $contact ]);
+ $alerts = $c->model('DB::Alert')->timeline_disabled( $c->cobrand->restriction );
- if ( %errors ) {
- $c->stash->{updated} = _('Please correct the errors below');
- $c->stash->{contact} = $contact;
- $c->stash->{errors} = \%errors;
- } elsif ( $contact->in_storage ) {
- $c->stash->{updated} = _('Values updated');
-
- # NB: History is automatically stored by a trigger in the database
- $contact->update;
- } else {
- $c->stash->{updated} = _('New category contact added');
- $contact->insert;
+ foreach ($alerts->all) {
+ push @{$time{$_->whendisabled->epoch}}, { type => 'alertDel', date => $_->whendisabled, obj => $_ };
}
+ } catch {
+ die $_;
+ } finally {
+ $c->model('DB')->schema->storage->sql_maker->quote_char( '' );
+ };
- unless ( %errors ) {
- $c->stash->{translation_col} = 'category';
- $c->stash->{object} = $contact;
- $c->forward('update_translations');
- }
-
- } elsif ( $posted eq 'update' ) {
- $c->forward('/auth/check_csrf_token');
-
- my @categories = $c->get_param_list('confirmed');
-
- my $contacts = $c->model('DB::Contact')->search(
- {
- body_id => $c->stash->{body_id},
- category => { -in => \@categories },
- }
- );
-
- $contacts->update(
- {
- state => 'confirmed',
- whenedited => \'current_timestamp',
- note => 'Confirmed',
- editor => $editor,
- }
- );
-
- $c->stash->{updated} = _('Values updated');
- } elsif ( $posted eq 'body' ) {
- $c->forward('check_for_super_user');
- $c->forward('/auth/check_csrf_token');
-
- my $values = $c->forward( 'body_params' );
- unless ( keys %{$c->stash->{body_errors}} ) {
- $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');
- foreach (@area_ids) {
- $c->model('DB::BodyArea')->find_or_create( { body => $c->stash->{body}, area_id => $_ } );
- delete $current{$_};
- }
- # Remove any others
- $c->stash->{body}->body_areas->search( { area_id => [ keys %current ] } )->delete;
-
- $c->stash->{translation_col} = 'name';
- $c->stash->{object} = $c->stash->{body};
- $c->forward('update_translations');
-
- $c->stash->{updated} = _('Values updated');
- }
- }
-}
-
-sub update_translations : Private {
- my ( $self, $c ) = @_;
-
- foreach my $lang (keys(%{$c->stash->{languages}})) {
- my $id = $c->get_param('translation_id_' . $lang);
- my $text = $c->get_param('translation_' . $lang);
- if ($id) {
- my $translation = $c->model('DB::Translation')->find(
- {
- id => $id,
- }
- );
-
- if ($text) {
- $translation->msgstr($text);
- $translation->update;
- } else {
- $translation->delete;
- }
- } elsif ($text) {
- my $col = $c->stash->{translation_col};
- $c->stash->{object}->add_translation_for(
- $col, $lang, $text
- );
- }
- }
-}
-
-sub body_params : Private {
- my ( $self, $c ) = @_;
-
- my @fields = qw/name endpoint jurisdiction api_key send_method external_url/;
- 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,
- can_be_devolved => 0,
- parent => undef,
- deleted => 0,
- );
- my %params = map { $_ => $c->get_param($_) || $defaults{$_} } keys %defaults;
- $c->forward('check_body_params', [ \%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 {
- my ( $self, $c, $params ) = @_;
-
- $c->stash->{body_errors} ||= {};
+ $c->stash->{time} = \%time;
- unless ($params->{name}) {
- $c->stash->{body_errors}->{name} = _('Please enter a name for this body');
- }
+ return 1;
}
sub fetch_contacts : Private {
@@ -522,125 +221,6 @@ sub fetch_languages : Private {
return 1;
}
-sub fetch_translations : Private {
- my ( $self, $c ) = @_;
-
- my $translations = {};
- if ($c->get_param('posted')) {
- foreach my $lang (keys %{$c->stash->{languages}}) {
- if (my $msgstr = $c->get_param('translation_' . $lang)) {
- $translations->{$lang} = { msgstr => $msgstr };
- }
- if (my $id = $c->get_param('translation_id_' . $lang)) {
- $translations->{$lang}->{id} = $id;
- }
- }
- } elsif ($c->stash->{object}) {
- my @translations = $c->stash->{object}->translation_for($c->stash->{translation_col})->all;
-
- foreach my $tx (@translations) {
- $translations->{$tx->lang} = { id => $tx->id, msgstr => $tx->msgstr };
- }
- }
-
- $c->stash->{translations} = $translations;
-}
-
-sub lookup_body : Private {
- my ( $self, $c, $body_id ) = @_;
-
- $c->stash->{body_id} = $body_id;
- my $body = $c->model('DB::Body')->find($body_id);
- $c->detach( '/page_error_404_not_found', [] )
- unless $body;
- $c->stash->{body} = $body;
-}
-
-sub body : Chained('/') : PathPart('admin/body') : CaptureArgs(1) {
- my ( $self, $c, $body_id ) = @_;
-
- $c->forward('lookup_body');
- my $body = $c->stash->{body};
-
- if ($body->body_areas->first) {
- my $example_postcode = mySociety::MaPit::call('area/example_postcode', $body->body_areas->first->area_id);
- if ($example_postcode && ! ref $example_postcode) {
- $c->stash->{example_pc} = $example_postcode;
- }
- }
-}
-
-sub edit_body : Chained('body') : PathPart('') : Args(0) {
- my ( $self, $c ) = @_;
-
- unless ($c->user->has_permission_to('category_edit', $c->stash->{body_id})) {
- $c->forward('check_for_super_user');
- }
-
- $c->forward( '/auth/get_csrf_token' );
- $c->forward( 'fetch_all_bodies' );
- $c->forward( 'body_form_dropdowns' );
- $c->forward('fetch_languages');
-
- if ( $c->get_param('posted') ) {
- $c->forward('update_contacts');
- }
-
- $c->stash->{object} = $c->stash->{body};
- $c->stash->{translation_col} = 'name';
-
- # if there's a contact then it's because we're displaying error
- # messages about adding a contact so grabbing translations will
- # fetch the contact submitted translations. So grab them, stash
- # them and then clear posted so we can fetch the body translations
- if ($c->stash->{contact}) {
- $c->forward('fetch_translations');
- $c->stash->{contact_translations} = $c->stash->{translations};
- }
- $c->set_param('posted', '');
-
- $c->forward('fetch_translations');
-
- # don't set this last as fetch_contacts might over-ride it
- # to display email addresses as text
- $c->stash->{template} = 'admin/body.html';
- $c->forward('fetch_contacts');
-
- return 1;
-}
-
-sub category : Chained('body') : PathPart('') {
- my ( $self, $c, @category ) = @_;
- my $category = join( '/', @category );
-
- $c->forward( '/auth/get_csrf_token' );
- $c->stash->{template} = 'admin/category_edit.html';
-
- my $contact = $c->stash->{body}->contacts->search( { category => $category } )->first;
- $c->stash->{contact} = $contact;
-
- $c->stash->{translation_col} = 'category';
- $c->stash->{object} = $c->stash->{contact};
-
- $c->forward('fetch_languages');
- $c->forward('fetch_translations');
-
- my $history = $c->model('DB::ContactsHistory')->search(
- {
- body_id => $c->stash->{body_id},
- category => $c->stash->{contact}->category
- },
- {
- order_by => ['contacts_history_id']
- },
- );
- $c->stash->{history} = $history;
- my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders };
- $c->stash->{send_methods} = \@methods;
-
- return 1;
-}
-
sub reports : Path('reports') {
my ( $self, $c ) = @_;
@@ -672,6 +252,15 @@ sub reports : Path('reports') {
return if $c->cobrand->call_hook(report_search_query => $query, $p_page, $u_page, $order);
if (my $search = $c->get_param('search')) {
+ $search = $self->trim($search);
+
+ # In case an email address, wrapped in <...>
+ if ($search =~ /^<(.*)>$/) {
+ my $possible_email = $1;
+ my $parsed = FixMyStreet::SMS->parse_username($possible_email);
+ $search = $possible_email if $parsed->{email};
+ }
+
$c->stash->{searched} = $search;
my $search_n = 0;
@@ -775,7 +364,7 @@ sub reports : Path('reports') {
-select => [ 'me.*', qw/problem.bodies_str problem.state/ ],
prefetch => [qw/problem/],
rows => 50,
- order_by => [ \"(me.state='hidden')", \"(problem.state='hidden')", 'me.created' ]
+ order_by => [ \"(me.state='hidden')", \"(problem.state='hidden')", { -desc => 'me.created' } ]
}
)->page( $u_page );
$c->stash->{updates} = [ $updates->all ];
@@ -822,6 +411,7 @@ sub report_edit_display : Private {
longitude => $problem->longitude,
colour => $c->cobrand->pin_colour($problem, 'admin'),
type => 'big',
+ draggable => 1,
} ]
: [],
print_report => 1,
@@ -870,11 +460,13 @@ sub report_edit : Path('report_edit') : Args(1) {
$c->forward('categories_for_point');
+ $c->forward('alerts_for_report');
+
$c->forward('check_username_for_abuse', [ $problem->user ] );
$c->stash->{updates} =
[ $c->model('DB::Comment')
- ->search( { problem_id => $problem->id }, { order_by => 'created' } )
+ ->search( { problem_id => $problem->id }, { order_by => [ 'created', 'id' ] } )
->all ];
if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) {
@@ -890,7 +482,7 @@ sub report_edit : Path('report_edit') : Args(1) {
if ( $c->get_param('resend') ) {
$c->forward('/auth/check_csrf_token');
- $problem->whensent(undef);
+ $problem->resend;
$problem->update();
$c->stash->{status_message} =
'<p><em>' . _('That problem will now be resent.') . '</em></p>';
@@ -904,15 +496,15 @@ sub report_edit : Path('report_edit') : Args(1) {
$c->forward( 'log_edit', [ $id, 'problem', 'marked sent' ] );
}
elsif ( $c->get_param('flaguser') ) {
- $c->forward('flag_user');
+ $c->forward('users/flag');
$c->stash->{problem}->discard_changes;
}
elsif ( $c->get_param('removeuserflag') ) {
- $c->forward('remove_user_flag');
+ $c->forward('users/flag_remove');
$c->stash->{problem}->discard_changes;
}
elsif ( $c->get_param('banuser') ) {
- $c->forward('ban_user');
+ $c->forward('users/ban');
}
elsif ( $c->get_param('submit') ) {
$c->forward('/auth/check_csrf_token');
@@ -957,10 +549,9 @@ sub report_edit : Path('report_edit') : Args(1) {
if ( $problem->state ne $old_state ) {
$c->forward( 'log_edit', [ $id, 'problem', 'state_change' ] );
- my $name = _('an administrator');
+ my $name = $c->user->moderating_user_name;
my $extra = { is_superuser => 1 };
if ($c->user->from_body) {
- $name = $c->user->from_body->name;
delete $extra->{is_superuser};
$extra->{is_body_user} = $c->user->from_body->id;
}
@@ -994,6 +585,7 @@ sub report_edit : Path('report_edit') : Args(1) {
=head2 report_edit_category
Handles changing a problem's category and the complexity that comes with it.
+Returns 1 if category changed, 0 if no change.
=cut
@@ -1008,7 +600,7 @@ sub report_edit_category : Private {
# If the report has changed bodies (and not to a subset!) we need to resend it
my %old_map = map { $_ => 1 } @{$problem->bodies_str_ids};
if (grep !$old_map{$_}, @new_body_ids) {
- $problem->whensent(undef);
+ $problem->resend;
}
# If the send methods of the old/new contacts differ we need to resend the report
my @new_send_methods = uniq map {
@@ -1019,7 +611,7 @@ sub report_edit_category : Private {
} @contacts;
my %old_send_methods = map { $_ => 1 } split /,/, ($problem->send_method_used || "Email");
if (grep !$old_send_methods{$_}, @new_send_methods) {
- $problem->whensent(undef);
+ $problem->resend;
}
$problem->bodies_str(join( ',', @new_body_ids ));
@@ -1038,7 +630,9 @@ sub report_edit_category : Private {
anonymous => 0,
});
}
+ return 1;
}
+ return 0;
}
=head2 report_edit_location
@@ -1047,7 +641,8 @@ Handles changing a problem's location and the complexity that comes with it.
For now, we reject the new location if the new location and old locations aren't
covered by the same body.
-Returns 1 if the new position (if any) is acceptable, undef otherwise.
+Returns 2 if the new position (if any) is acceptable and changed,
+1 if acceptable and unchanged, undef otherwise.
NB: This must be called before report_edit_category, as that might modify
$problem->bodies_str.
@@ -1067,6 +662,8 @@ sub report_edit_location : Private {
# this lookup is bad. So let's save the stash and restore it after the
# comparison.
my $safe_stash = { %{$c->stash} };
+ $c->stash->{fetch_all_areas} = 1;
+ $c->stash->{area_check_action} = 'admin';
$c->forward('/council/load_and_check_areas', []);
$c->forward('/report/new/setup_categories_and_bodies');
my %allowed_bodies = map { $_ => 1 } @{$problem->bodies_str_ids};
@@ -1076,6 +673,9 @@ sub report_edit_location : Private {
return unless $bodies_match;
$problem->latitude($c->stash->{latitude});
$problem->longitude($c->stash->{longitude});
+ my $areas = $c->stash->{all_areas_mapit};
+ $problem->areas( ',' . join( ',', sort keys %$areas ) . ',' );
+ return 2;
}
return 1;
}
@@ -1098,6 +698,17 @@ sub categories_for_point : Private {
$c->stash->{categories_hash} = { map { $_->category => 1 } @{$c->stash->{category_options}} };
}
+sub alerts_for_report : Private {
+ my ($self, $c) = @_;
+
+ $c->stash->{alert_count} = $c->model('DB::Alert')->search({
+ alert_type => 'new_updates',
+ parameter => $c->stash->{report}->id,
+ confirmed => 1,
+ whendisabled => undef,
+ })->count();
+}
+
sub templates : Path('templates') : Args(0) {
my ( $self, $c ) = @_;
@@ -1249,46 +860,6 @@ sub load_template_body : Private {
or $c->detach( '/page_error_404_not_found', [] );
}
-sub users: Path('users') : Args(0) {
- my ( $self, $c ) = @_;
-
- if (my $search = $c->get_param('search')) {
- $c->stash->{searched} = $search;
-
- my $isearch = '%' . $search . '%';
- my $search_n = 0;
- $search_n = int($search) if $search =~ /^\d+$/;
-
- my $users = $c->cobrand->users->search(
- {
- -or => [
- email => { ilike => $isearch },
- phone => { ilike => $isearch },
- name => { ilike => $isearch },
- from_body => $search_n,
- ]
- }
- );
- my @users = $users->all;
- $c->stash->{users} = [ @users ];
- $c->forward('add_flags', [ { email => { ilike => $isearch } } ]);
-
- } else {
- $c->forward('/auth/get_csrf_token');
- $c->forward('fetch_all_bodies');
-
- # Admin users by default
- my $users = $c->cobrand->users->search(
- { from_body => { '!=', undef } },
- { order_by => 'name' }
- );
- my @users = $users->all;
- $c->stash->{users} = \@users;
- }
-
- return 1;
-}
-
sub update_edit : Path('update_edit') : Args(1) {
my ( $self, $c, $id ) = @_;
@@ -1309,14 +880,14 @@ sub update_edit : Path('update_edit') : Args(1) {
$c->forward('check_username_for_abuse', [ $update->user ] );
if ( $c->get_param('banuser') ) {
- $c->forward('ban_user');
+ $c->forward('users/ban');
}
elsif ( $c->get_param('flaguser') ) {
- $c->forward('flag_user');
+ $c->forward('users/flag');
$c->stash->{update}->discard_changes;
}
elsif ( $c->get_param('removeuserflag') ) {
- $c->forward('remove_user_flag');
+ $c->forward('users/flag_remove');
$c->stash->{update}->discard_changes;
}
elsif ( $c->get_param('submit') ) {
@@ -1383,366 +954,6 @@ sub update_edit : Path('update_edit') : Args(1) {
return 1;
}
-sub phone_check : Private {
- my ($self, $c, $phone) = @_;
- my $parsed = FixMyStreet::SMS->parse_username($phone);
- if ($parsed->{phone} && $parsed->{may_be_mobile}) {
- return $parsed->{username};
- } elsif ($parsed->{phone}) {
- $c->stash->{field_errors}->{phone} = _('Please enter a mobile number');
- } else {
- $c->stash->{field_errors}->{phone} = _('Please check your phone number is correct');
- }
-}
-
-sub user_add : Path('user_edit') : Args(0) {
- my ( $self, $c ) = @_;
-
- $c->stash->{template} = 'admin/user_edit.html';
- $c->forward('/auth/get_csrf_token');
- $c->forward('fetch_all_bodies');
-
- return unless $c->get_param('submit');
-
- $c->forward('/auth/check_csrf_token');
-
- $c->stash->{field_errors} = {};
- my $email = lc $c->get_param('email');
- my $phone = $c->get_param('phone');
- my $email_v = $c->get_param('email_verified');
- my $phone_v = $c->get_param('phone_verified');
-
- unless ($email || $phone) {
- $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number');
- }
- if (!$email_v && !$phone_v) {
- $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone');
- }
- if ($email && !is_valid_email($email)) {
- $c->stash->{field_errors}->{email} = _('Please enter a valid email');
- }
- unless ($c->get_param('name')) {
- $c->stash->{field_errors}->{name} = _('Please enter a name');
- }
-
- if ($phone_v) {
- my $parsed_phone = $c->forward('phone_check', [ $phone ]);
- $phone = $parsed_phone if $parsed_phone;
- }
-
- my $existing_email = $email_v && $c->model('DB::User')->find( { email => $email } );
- my $existing_phone = $phone_v && $c->model('DB::User')->find( { phone => $phone } );
- if ($existing_email || $existing_phone) {
- $c->stash->{field_errors}->{username} = _('User already exists');
- }
-
- return if %{$c->stash->{field_errors}};
-
- my $user = $c->model('DB::User')->create( {
- name => $c->get_param('name'),
- email => $email ? $email : undef,
- email_verified => $email && $email_v ? 1 : 0,
- phone => $phone || undef,
- phone_verified => $phone && $phone_v ? 1 : 0,
- from_body => $c->get_param('body') || undef,
- flagged => $c->get_param('flagged') || 0,
- # Only superusers can create superusers
- is_superuser => ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0,
- } );
- $c->stash->{user} = $user;
- $c->forward('user_cobrand_extra_fields');
- $user->update;
-
- $c->forward( 'log_edit', [ $user->id, 'user', 'edit' ] );
-
- $c->flash->{status_message} = _("Updated!");
- $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) );
-}
-
-sub user_edit : Path('user_edit') : Args(1) {
- my ( $self, $c, $id ) = @_;
-
- $c->forward('/auth/get_csrf_token');
-
- my $user = $c->cobrand->users->find( { id => $id } );
- $c->detach( '/page_error_404_not_found', [] ) unless $user;
-
- unless ( $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) {
- $c->detach('/page_error_403_access_denied', []);
- }
-
- $c->stash->{user} = $user;
- $c->forward( 'check_username_for_abuse', [ $user ] );
-
- if ( $user->from_body && $c->user->has_permission_to('user_manage_permissions', $user->from_body->id) ) {
- $c->stash->{available_permissions} = $c->cobrand->available_permissions;
- }
-
- $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>';
- }
-
- $c->forward('/auth/check_csrf_token') if $c->get_param('submit');
-
- if ( $c->get_param('submit') and $c->get_param('unban') ) {
- $c->forward('unban_user', [ $user ]);
- } elsif ( $c->get_param('submit') and $c->get_param('logout_everywhere') ) {
- $c->forward('user_logout_everywhere', [ $user ]);
- } elsif ( $c->get_param('submit') and $c->get_param('anon_everywhere') ) {
- $c->forward('user_anon_everywhere', [ $user ]);
- } elsif ( $c->get_param('submit') and $c->get_param('hide_everywhere') ) {
- $c->forward('user_hide_everywhere', [ $user ]);
- } elsif ( $c->get_param('submit') and $c->get_param('remove_account') ) {
- $c->forward('user_remove_account', [ $user ]);
- } elsif ( $c->get_param('submit') ) {
-
- my $edited = 0;
-
- my $name = $c->get_param('name');
- my $email = lc $c->get_param('email');
- my $phone = $c->get_param('phone');
- my $email_v = $c->get_param('email_verified') || 0;
- my $phone_v = $c->get_param('phone_verified') || 0;
-
- $c->stash->{field_errors} = {};
-
- unless ($email || $phone) {
- $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number');
- }
- if (!$email_v && !$phone_v) {
- $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone');
- }
- if ($email && !is_valid_email($email)) {
- $c->stash->{field_errors}->{email} = _('Please enter a valid email');
- }
-
- if ($phone_v) {
- my $parsed_phone = $c->forward('phone_check', [ $phone ]);
- $phone = $parsed_phone if $parsed_phone;
- }
-
- unless ($name) {
- $c->stash->{field_errors}->{name} = _('Please enter a name');
- }
-
- my $email_params = { email => $email, email_verified => 1, id => { '!=', $user->id } };
- my $phone_params = { phone => $phone, phone_verified => 1, id => { '!=', $user->id } };
- my $existing_email = $email_v && $c->model('DB::User')->search($email_params)->first;
- my $existing_phone = $phone_v && $c->model('DB::User')->search($phone_params)->first;
- my $existing_user = $existing_email || $existing_phone;
- my $existing_email_cobrand = $email_v && $c->cobrand->users->search($email_params)->first;
- my $existing_phone_cobrand = $phone_v && $c->cobrand->users->search($phone_params)->first;
- my $existing_user_cobrand = $existing_email_cobrand || $existing_phone_cobrand;
- if ($existing_phone_cobrand && $existing_email_cobrand && $existing_email_cobrand->id != $existing_phone_cobrand->id) {
- $c->stash->{field_errors}->{username} = _('User already exists');
- }
-
- return if %{$c->stash->{field_errors}};
-
- if ( ($user->email || "") ne $email ||
- $user->name ne $name ||
- ($user->phone || "") ne $phone ||
- ($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) ||
- (!$user->from_body && $c->get_param('body'))
- ) {
- $edited = 1;
- }
-
- if ($existing_user_cobrand) {
- $existing_user->adopt($user);
- $c->forward( 'log_edit', [ $id, 'user', 'merge' ] );
- return $c->res->redirect( $c->uri_for( 'user_edit', $existing_user->id ) );
- }
-
- $user->email($email) if !$existing_email;
- $user->phone($phone) if !$existing_phone;
- $user->email_verified( $email_v );
- $user->phone_verified( $phone_v );
- $user->name( $name );
-
- $user->flagged( $c->get_param('flagged') || 0 );
- # Only superusers can grant superuser status
- $user->is_superuser( ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0 );
- # Superusers can set from_body to any value, but other staff can only
- # set from_body to the same value as their own from_body.
- if ( $c->user->is_superuser || $c->cobrand->moniker eq 'zurich' ) {
- $user->from_body( $c->get_param('body') || undef );
- } elsif ( $c->user->has_body_permission_to('user_assign_body') &&
- $c->get_param('body') && $c->get_param('body') eq $c->user->from_body->id ) {
- $user->from_body( $c->user->from_body );
- } else {
- $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} ) {
- $c->forward('fetch_body_areas', [ $user->from_body ]);
- }
-
- if (!$user->from_body) {
- # Non-staff users aren't allowed any permissions or to be in an area
- $user->admin_user_body_permissions->delete;
- $user->area_id(undef);
- delete $c->stash->{areas};
- delete $c->stash->{fetched_areas_body_id};
- } elsif ($c->stash->{available_permissions}) {
- my @all_permissions = map { keys %$_ } values %{ $c->stash->{available_permissions} };
- my @user_permissions = grep { $c->get_param("permissions[$_]") ? 1 : undef } @all_permissions;
- $user->admin_user_body_permissions->search({
- body_id => $user->from_body->id,
- permission_type => { '!=' => \@user_permissions },
- })->delete;
- foreach my $permission_type (@user_permissions) {
- $user->user_body_permissions->find_or_create({
- body_id => $user->from_body->id,
- permission_type => $permission_type,
- });
- }
- }
-
- if ( $user->from_body && $c->user->has_permission_to('user_assign_areas', $user->from_body->id) ) {
- my %valid_areas = map { $_->{id} => 1 } @{ $c->stash->{areas} };
- my $new_area = $c->get_param('area_id');
- $user->area_id( $valid_areas{$new_area} ? $new_area : undef );
- }
-
- # Handle 'trusted' flag(s)
- my @trusted_bodies = $c->get_param_list('trusted_bodies');
- if ( $c->user->is_superuser ) {
- $user->user_body_permissions->search({
- body_id => { '!=' => \@trusted_bodies },
- permission_type => 'trusted',
- })->delete;
- foreach my $body_id (@trusted_bodies) {
- $user->user_body_permissions->find_or_create({
- body_id => $body_id,
- permission_type => 'trusted',
- });
- }
- } elsif ( $c->user->from_body ) {
- my %trusted = map { $_ => 1 } @trusted_bodies;
- my $body_id = $c->user->from_body->id;
- if ( $trusted{$body_id} ) {
- $user->user_body_permissions->find_or_create({
- body_id => $body_id,
- permission_type => 'trusted',
- });
- } else {
- $user->user_body_permissions->search({
- body_id => $body_id,
- permission_type => 'trusted',
- })->delete;
- }
- }
-
- # Update the categories this user operates in
- if ( $user->from_body ) {
- $c->stash->{body} = $user->from_body;
- $c->forward('fetch_contacts');
- my @live_contacts = $c->stash->{live_contacts}->all;
- my @live_contact_ids = map { $_->id } @live_contacts;
- my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids;
- $user->set_extra_metadata('categories', \@new_contact_ids);
- }
-
- $user->update;
- if ($edited) {
- $c->forward( 'log_edit', [ $id, 'user', 'edit' ] );
- }
- $c->flash->{status_message} = _("Updated!");
- return $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) );
- }
-
- if ( $user->from_body ) {
- unless ( $c->stash->{live_contacts} ) {
- $c->stash->{body} = $user->from_body;
- $c->forward('fetch_contacts');
- }
- my @contacts = @{$user->get_extra_metadata('categories') || []};
- my %active_contacts = map { $_ => 1 } @contacts;
- my @live_contacts = $c->stash->{live_contacts}->all;
- my @all_contacts = map { {
- id => $_->id,
- category => $_->category,
- active => $active_contacts{$_->id},
- } } @live_contacts;
- $c->stash->{contacts} = \@all_contacts;
- }
-
- 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 ) = @_;
-
- my @extra_fields = @{ $c->cobrand->call_hook('user_extra_fields') || [] };
- foreach ( @extra_fields ) {
- $c->stash->{user}->set_extra_metadata( $_ => $c->get_param("extra[$_]") );
- }
-}
-
sub add_flags : Private {
my ( $self, $c, $search ) = @_;
@@ -1841,165 +1052,6 @@ sub log_edit : Private {
)->insert();
}
-=head2 ban_user
-
-Add the user's email address/phone number to the abuse table if they are not
-already in there and sets status_message accordingly.
-
-=cut
-
-sub ban_user : Private {
- my ( $self, $c ) = @_;
-
- my $user;
- if ($c->stash->{problem}) {
- $user = $c->stash->{problem}->user;
- } elsif ($c->stash->{update}) {
- $user = $c->stash->{update}->user;
- }
- return unless $user;
-
- if ($user->email_verified && $user->email) {
- my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->email });
- if ( $abuse->in_storage ) {
- $c->stash->{status_message} = _('User already in abuse list');
- } else {
- $abuse->insert;
- $c->stash->{status_message} = _('User added to abuse list');
- }
- $c->stash->{username_in_abuse} = 1;
- }
- if ($user->phone_verified && $user->phone) {
- my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->phone });
- if ( $abuse->in_storage ) {
- $c->stash->{status_message} = _('User already in abuse list');
- } else {
- $abuse->insert;
- $c->stash->{status_message} = _('User added to abuse list');
- }
- $c->stash->{username_in_abuse} = 1;
- }
- return 1;
-}
-
-sub user_logout_everywhere : Private {
- my ( $self, $c, $user ) = @_;
- my $sessions = $user->get_extra_metadata('sessions');
- foreach (grep { $_ ne $c->sessionid } @$sessions) {
- $c->delete_session_data("session:$_");
- }
- $c->stash->{status_message} = _('That user has been logged out.');
-}
-
-sub user_anon_everywhere : Private {
- my ( $self, $c, $user ) = @_;
- $user->problems->update({anonymous => 1});
- $user->comments->update({anonymous => 1});
- $c->stash->{status_message} = _('That user has been made anonymous on all reports and updates.');
-}
-
-sub user_hide_everywhere : Private {
- my ( $self, $c, $user ) = @_;
- my $problems = $user->problems->search({ state => { '!=' => 'hidden' } });
- while (my $problem = $problems->next) {
- $problem->get_photoset->delete_cached;
- $problem->update({ state => 'hidden' });
- }
- my $updates = $user->comments->search({ state => { '!=' => 'hidden' } });
- while (my $update = $updates->next) {
- $update->hide;
- }
- $c->stash->{status_message} = _('That user’s reports and updates have been hidden.');
-}
-
-# Anonymize and remove name from all problems/updates, disable all alerts.
-# Remove their account's email address, phone number, password, etc.
-sub user_remove_account : Private {
- my ( $self, $c, $user ) = @_;
- $c->forward('user_logout_everywhere', [ $user ]);
- $user->anonymize_account;
- $c->stash->{status_message} = _('That user’s personal details have been removed.');
-}
-
-sub unban_user : Private {
- my ( $self, $c, $user ) = @_;
-
- my @username;
- if ($user->email_verified && $user->email) {
- push @username, $user->email;
- }
- if ($user->phone_verified && $user->phone) {
- push @username, $user->phone;
- }
- if (@username) {
- my $abuse = $c->model('DB::Abuse')->search({ email => \@username });
- if ( $abuse ) {
- $abuse->delete;
- $c->stash->{status_message} = _('user removed from abuse list');
- } else {
- $c->stash->{status_message} = _('user not in abuse list');
- }
- $c->stash->{username_in_abuse} = 0;
- }
-}
-
-=head2 flag_user
-
-Sets the flag on a user
-
-=cut
-
-sub flag_user : Private {
- my ( $self, $c ) = @_;
-
- my $user;
- if ($c->stash->{problem}) {
- $user = $c->stash->{problem}->user;
- } elsif ($c->stash->{update}) {
- $user = $c->stash->{update}->user;
- }
-
- if ( !$user ) {
- $c->stash->{status_message} = _('Could not find user');
- } else {
- $user->flagged(1);
- $user->update;
- $c->stash->{status_message} = _('User flagged');
- }
-
- $c->stash->{user_flagged} = 1;
-
- return 1;
-}
-
-=head2 remove_user_flag
-
-Remove the flag on a user
-
-=cut
-
-sub remove_user_flag : Private {
- my ( $self, $c ) = @_;
-
- my $user;
- if ($c->stash->{problem}) {
- $user = $c->stash->{problem}->user;
- } elsif ($c->stash->{update}) {
- $user = $c->stash->{update}->user;
- }
-
- if ( !$user ) {
- $c->stash->{status_message} = _('Could not find user');
- } else {
- $user->flagged(0);
- $user->update;
- $c->stash->{status_message} = _('User flag removed');
- }
-
- return 1;
-}
-
-
=head2 check_username_for_abuse
$c->forward('check_username_for_abuse', [ $user ] );
@@ -2101,10 +1153,15 @@ sub check_page_allowed : Private {
sub fetch_all_bodies : Private {
my ($self, $c ) = @_;
- my @bodies = $c->model('DB::Body')->translated->all_sorted;
- if ( $c->cobrand->moniker eq 'zurich' ) {
- @bodies = $c->cobrand->admin_fetch_all_bodies( @bodies );
+ my @bodies = $c->cobrand->call_hook('admin_fetch_all_bodies');
+ if (!@bodies) {
+ my $bodies = $c->model('DB::Body')->search(undef, {
+ columns => [ "id", "name", "deleted", "parent" ],
+ })->with_parent_name;
+ $bodies = $bodies->with_defect_type_count if $c->stash->{with_defect_type_count};
+ @bodies = $bodies->translated->all_sorted;
}
+
$c->stash->{bodies} = \@bodies;
return 1;
@@ -2145,6 +1202,8 @@ sub update_extra_fields : Private {
$meta->{variable} = $notice ? 'false' : 'true';
$meta->{description} = $c->get_param("metadata[$i].description");
$meta->{datatype_description} = $c->get_param("metadata[$i].datatype_description");
+ $meta->{automated} = $c->get_param("metadata[$i].automated")
+ if $c->get_param("metadata[$i].automated");
if ( $meta->{datatype} eq "singlevaluelist" ) {
$meta->{values} = [];
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm
new file mode 100644
index 000000000..0e47d2238
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm
@@ -0,0 +1,476 @@
+package FixMyStreet::App::Controller::Admin::Bodies;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use POSIX qw(strcoll);
+use mySociety::EmailUtil qw(is_valid_email_list);
+use FixMyStreet::MapIt;
+use FixMyStreet::SendReport;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Admin::Bodies - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Admin pages
+
+=head1 METHODS
+
+=cut
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ if (my $body_id = $c->get_param('body')) {
+ return $c->res->redirect( $c->uri_for_action('admin/bodies/edit', [ $body_id ] ) );
+ }
+
+ if (!$c->user->is_superuser && $c->user->from_body && $c->cobrand->moniker ne 'zurich') {
+ return $c->res->redirect( $c->uri_for_action('admin/bodies/edit', [ $c->user->from_body->id ] ) );
+ }
+
+ $c->forward( '/auth/get_csrf_token' );
+
+ my $edit_activity = $c->model('DB::ContactsHistory')->search(
+ undef,
+ {
+ select => [ 'editor', { count => 'contacts_history_id', -as => 'c' } ],
+ as => [ 'editor', 'c' ],
+ group_by => ['editor'],
+ order_by => { -desc => 'c' }
+ }
+ );
+
+ $c->stash->{edit_activity} = $edit_activity;
+
+ $c->forward( '/admin/fetch_languages' );
+ $c->forward( 'fetch_translations' );
+
+ my $posted = $c->get_param('posted') || '';
+ if ( $posted eq 'body' ) {
+ $c->forward('check_for_super_user');
+ $c->forward('/auth/check_csrf_token');
+
+ my $values = $c->forward('body_params');
+ unless ( keys %{$c->stash->{body_errors}} ) {
+ 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 => $_ } );
+ }
+
+ $c->stash->{object} = $body;
+ $c->stash->{translation_col} = 'name';
+ $c->forward('update_translations');
+ $c->stash->{updated} = _('New body added');
+ }
+ }
+
+ $c->forward( '/admin/fetch_all_bodies' );
+
+ my $contacts = $c->model('DB::Contact')->search(
+ undef,
+ {
+ select => [ 'body_id', { count => 'id' }, { count => \'case when state = \'deleted\' then 1 else null end' },
+ { count => \'case when state = \'confirmed\' then 1 else null end' } ],
+ as => [qw/body_id c deleted confirmed/],
+ group_by => [ 'body_id' ],
+ result_class => 'DBIx::Class::ResultClass::HashRefInflator'
+ }
+ );
+
+ my %council_info = map { $_->{body_id} => $_ } $contacts->all;
+
+ $c->stash->{counts} = \%council_info;
+
+ $c->forward( 'body_form_dropdowns' );
+
+ return 1;
+}
+
+sub body : Chained('/') : PathPart('admin/body') : CaptureArgs(1) {
+ my ( $self, $c, $body_id ) = @_;
+
+ $c->stash->{body_id} = $body_id;
+ my $body = $c->model('DB::Body')->find($body_id);
+ $c->detach( '/page_error_404_not_found', [] ) unless $body;
+ $c->stash->{body} = $body;
+
+ if ($body->body_areas->first) {
+ my $example_postcode = FixMyStreet::MapIt::call('area/example_postcode', $body->body_areas->first->area_id);
+ if ($example_postcode && ! ref $example_postcode) {
+ $c->stash->{example_pc} = $example_postcode;
+ }
+ }
+}
+
+sub edit : Chained('body') : PathPart('') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ unless ($c->user->has_permission_to('category_edit', $c->stash->{body_id})) {
+ $c->forward('check_for_super_user');
+ }
+
+ $c->forward( '/auth/get_csrf_token' );
+ $c->forward( '/admin/fetch_all_bodies' );
+ $c->forward( 'body_form_dropdowns' );
+ $c->forward('/admin/fetch_languages');
+
+ if ( $c->get_param('posted') ) {
+ $c->forward('update_contacts');
+ }
+
+ $c->stash->{object} = $c->stash->{body};
+ $c->stash->{translation_col} = 'name';
+
+ # if there's a contact then it's because we're displaying error
+ # messages about adding a contact so grabbing translations will
+ # fetch the contact submitted translations. So grab them, stash
+ # them and then clear posted so we can fetch the body translations
+ if ($c->stash->{contact}) {
+ $c->forward('fetch_translations');
+ $c->stash->{contact_translations} = $c->stash->{translations};
+ }
+ $c->set_param('posted', '');
+
+ $c->forward('fetch_translations');
+
+ # don't set this last as fetch_contacts might over-ride it
+ # to display email addresses as text
+ $c->stash->{template} = 'admin/bodies/body.html';
+ $c->forward('/admin/fetch_contacts');
+
+ return 1;
+}
+
+sub category : Chained('body') : PathPart('') {
+ my ( $self, $c, @category ) = @_;
+ my $category = join( '/', @category );
+
+ $c->forward( '/auth/get_csrf_token' );
+
+ my $contact = $c->stash->{body}->contacts->search( { category => $category } )->first;
+ $c->stash->{contact} = $contact;
+
+ $c->stash->{translation_col} = 'category';
+ $c->stash->{object} = $c->stash->{contact};
+
+ $c->forward('/admin/fetch_languages');
+ $c->forward('fetch_translations');
+
+ my $history = $c->model('DB::ContactsHistory')->search(
+ {
+ body_id => $c->stash->{body_id},
+ category => $c->stash->{contact}->category
+ },
+ {
+ order_by => ['contacts_history_id']
+ },
+ );
+ $c->stash->{history} = $history;
+ my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders };
+ $c->stash->{send_methods} = \@methods;
+
+ return 1;
+}
+
+sub body_form_dropdowns : Private {
+ my ( $self, $c ) = @_;
+
+ my $areas;
+ my $whitelist = $c->config->{MAPIT_ID_WHITELIST};
+
+ if ( $whitelist && ref $whitelist eq 'ARRAY' && @$whitelist ) {
+ $areas = FixMyStreet::MapIt::call('areas', $whitelist);
+ } else {
+ $areas = FixMyStreet::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:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders };
+ $c->stash->{send_methods} = \@methods;
+}
+
+sub check_for_super_user : Private {
+ my ( $self, $c ) = @_;
+
+ my $superuser = $c->user->is_superuser;
+ # Zurich currently has its own way of defining superusers
+ $superuser ||= $c->cobrand->moniker eq 'zurich' && $c->stash->{admin_type} eq 'super';
+
+ unless ( $superuser ) {
+ $c->detach('/page_error_403_access_denied', []);
+ }
+}
+
+sub update_contacts : Private {
+ my ( $self, $c ) = @_;
+
+ my $posted = $c->get_param('posted');
+ my $editor = $c->forward('/admin/get_user');
+
+ if ( $posted eq 'new' ) {
+ $c->forward('/auth/check_csrf_token');
+
+ my %errors;
+
+ my $category = $self->trim( $c->get_param('category') );
+ $errors{category} = _("Please choose a category") unless $category;
+ $errors{note} = _('Please enter a message') unless $c->get_param('note');
+
+ my $contact = $c->model('DB::Contact')->find_or_new(
+ {
+ body_id => $c->stash->{body_id},
+ category => $category,
+ }
+ );
+
+ 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_list($email) || $email eq 'REFUSED';
+ }
+
+ $contact->email( $email );
+ $contact->state( $c->get_param('state') );
+ $contact->non_public( $c->get_param('non_public') ? 1 : 0 );
+ $contact->note( $c->get_param('note') );
+ $contact->whenedited( \'current_timestamp' );
+ $contact->editor( $editor );
+ $contact->endpoint( $c->get_param('endpoint') );
+ $contact->jurisdiction( $c->get_param('jurisdiction') );
+ $contact->api_key( $c->get_param('api_key') );
+ $contact->send_method( $c->get_param('send_method') );
+
+ # Set flags in extra to the appropriate values
+ if ( $c->get_param('photo_required') ) {
+ $contact->set_extra_metadata_if_undefined( photo_required => 1 );
+ }
+ else {
+ $contact->unset_extra_metadata( 'photo_required' );
+ }
+ if ( $c->get_param('inspection_required') ) {
+ $contact->set_extra_metadata( inspection_required => 1 );
+ }
+ else {
+ $contact->unset_extra_metadata( 'inspection_required' );
+ }
+ if ( $c->get_param('reputation_threshold') ) {
+ $contact->set_extra_metadata( reputation_threshold => int($c->get_param('reputation_threshold')) );
+ }
+ if ( my $group = $c->get_param('group') ) {
+ $contact->set_extra_metadata( group => $group );
+ } else {
+ $contact->unset_extra_metadata( 'group' );
+ }
+
+
+ $c->forward('/admin/update_extra_fields', [ $contact ]);
+ $c->forward('contact_cobrand_extra_fields', [ $contact ]);
+
+ if ( %errors ) {
+ $c->stash->{updated} = _('Please correct the errors below');
+ $c->stash->{contact} = $contact;
+ $c->stash->{errors} = \%errors;
+ } elsif ( $contact->in_storage ) {
+ $c->stash->{updated} = _('Values updated');
+
+ # NB: History is automatically stored by a trigger in the database
+ $contact->update;
+ } else {
+ $c->stash->{updated} = _('New category contact added');
+ $contact->insert;
+ }
+
+ unless ( %errors ) {
+ $c->stash->{translation_col} = 'category';
+ $c->stash->{object} = $contact;
+ $c->forward('update_translations');
+ }
+
+ } elsif ( $posted eq 'update' ) {
+ $c->forward('/auth/check_csrf_token');
+
+ my @categories = $c->get_param_list('confirmed');
+
+ my $contacts = $c->model('DB::Contact')->search(
+ {
+ body_id => $c->stash->{body_id},
+ category => { -in => \@categories },
+ }
+ );
+
+ $contacts->update(
+ {
+ state => 'confirmed',
+ whenedited => \'current_timestamp',
+ note => 'Confirmed',
+ editor => $editor,
+ }
+ );
+
+ $c->stash->{updated} = _('Values updated');
+ } elsif ( $posted eq 'body' ) {
+ $c->forward('check_for_super_user');
+ $c->forward('/auth/check_csrf_token');
+
+ my $values = $c->forward( 'body_params' );
+ unless ( keys %{$c->stash->{body_errors}} ) {
+ $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');
+ foreach (@area_ids) {
+ $c->model('DB::BodyArea')->find_or_create( { body => $c->stash->{body}, area_id => $_ } );
+ delete $current{$_};
+ }
+ # Remove any others
+ $c->stash->{body}->body_areas->search( { area_id => [ keys %current ] } )->delete;
+
+ $c->stash->{translation_col} = 'name';
+ $c->stash->{object} = $c->stash->{body};
+ $c->forward('update_translations');
+
+ $c->stash->{updated} = _('Values updated');
+ }
+ }
+}
+
+sub body_params : Private {
+ my ( $self, $c ) = @_;
+
+ my @fields = qw/name endpoint jurisdiction api_key send_method external_url/;
+ 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,
+ can_be_devolved => 0,
+ parent => undef,
+ deleted => 0,
+ );
+ my %params = map { $_ => $c->get_param($_) || $defaults{$_} } keys %defaults;
+ $c->forward('check_body_params', [ \%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 {
+ my ( $self, $c, $params ) = @_;
+
+ $c->stash->{body_errors} ||= {};
+
+ unless ($params->{name}) {
+ $c->stash->{body_errors}->{name} = _('Please enter a name for this body');
+ }
+}
+
+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 fetch_translations : Private {
+ my ( $self, $c ) = @_;
+
+ my $translations = {};
+ if ($c->get_param('posted')) {
+ foreach my $lang (keys %{$c->stash->{languages}}) {
+ if (my $msgstr = $c->get_param('translation_' . $lang)) {
+ $translations->{$lang} = { msgstr => $msgstr };
+ }
+ if (my $id = $c->get_param('translation_id_' . $lang)) {
+ $translations->{$lang}->{id} = $id;
+ }
+ }
+ } elsif ($c->stash->{object}) {
+ my @translations = $c->stash->{object}->translation_for($c->stash->{translation_col})->all;
+
+ foreach my $tx (@translations) {
+ $translations->{$tx->lang} = { id => $tx->id, msgstr => $tx->msgstr };
+ }
+ }
+
+ $c->stash->{translations} = $translations;
+}
+
+sub update_translations : Private {
+ my ( $self, $c ) = @_;
+
+ foreach my $lang (keys(%{$c->stash->{languages}})) {
+ my $id = $c->get_param('translation_id_' . $lang);
+ my $text = $c->get_param('translation_' . $lang);
+ if ($id) {
+ my $translation = $c->model('DB::Translation')->find(
+ {
+ id => $id,
+ }
+ );
+
+ if ($text) {
+ $translation->msgstr($text);
+ $translation->update;
+ } else {
+ $translation->delete;
+ }
+ } elsif ($text) {
+ my $col = $c->stash->{translation_col};
+ $c->stash->{object}->add_translation_for(
+ $col, $lang, $text
+ );
+ }
+ }
+}
+
+sub trim {
+ my $self = shift;
+ my $e = shift;
+ $e =~ s/^\s+//;
+ $e =~ s/\s+$//;
+ return $e;
+}
+
+=head1 AUTHOR
+
+Struan Donald
+
+=head1 LICENSE
+
+This library is free software. You can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
index 5dab1da2c..ed9b40fd0 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
@@ -12,6 +12,7 @@ sub index : Path : Args(0) {
my $user = $c->user;
if ($user->is_superuser) {
+ $c->stash->{with_defect_type_count} = 1;
$c->forward('/admin/fetch_all_bodies');
} elsif ( $user->from_body ) {
$c->forward('load_user_body', [ $user->from_body->id ]);
diff --git a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
index d965dd8f2..0026acb9c 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
@@ -5,6 +5,7 @@ use namespace::autoclean;
use DateTime;
use Try::Tiny;
use FixMyStreet::Integrations::ExorRDI;
+use FixMyStreet::DateRange;
BEGIN { extends 'Catalyst::Controller'; }
@@ -43,15 +44,16 @@ sub download : Path('download') : Args(0) {
$c->detach( '/page_error_404_not_found', [] );
}
- 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 );
+ my $range = FixMyStreet::DateRange->new(
+ start_date => $c->get_param('start_date'),
+ end_date => $c->get_param('end_date'),
+ parser => DateTime::Format::Strptime->new( pattern => '%Y-%m-%d' ),
+ );
my $params = {
- start_date => $start_date,
- inspection_date => $start_date,
- end_date => $end_date + $one_day,
+ start_date => $range->start,
+ inspection_date => $range->start,
+ end_date => $range->end,
user => $c->get_param('user_id'),
mark_as_processed => 0,
};
diff --git a/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm b/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm
index 337fb4bed..0ddbb01f7 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm
@@ -9,7 +9,7 @@ BEGIN { extends 'Catalyst::Controller'; }
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
- my @extras = $c->model('DB::ReportExtraFields')->search(
+ my @extras = $c->model('DB::ReportExtraField')->search(
undef,
{
order_by => 'name'
@@ -24,9 +24,9 @@ sub edit : Path : Args(1) {
my $extra;
if ( $extra_id eq 'new' ) {
- $extra = $c->model('DB::ReportExtraFields')->new({});
+ $extra = $c->model('DB::ReportExtraField')->new({});
} else {
- $extra = $c->model('DB::ReportExtraFields')->find( $extra_id )
+ $extra = $c->model('DB::ReportExtraField')->find( $extra_id )
or $c->detach( '/page_error_404_not_found' );
}
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Users.pm b/perllib/FixMyStreet/App/Controller/Admin/Users.pm
new file mode 100644
index 000000000..bcbc808ed
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/Users.pm
@@ -0,0 +1,685 @@
+package FixMyStreet::App::Controller::Admin::Users;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use POSIX qw(strcoll);
+use mySociety::EmailUtil qw(is_valid_email);
+use Text::CSV;
+
+use FixMyStreet::MapIt;
+use FixMyStreet::SMS;
+use Utils;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Admin::Users - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Admin pages for editing users
+
+=head1 METHODS
+
+=cut
+
+sub index :Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->detach('add') if $c->req->method eq 'POST'; # Add a user
+
+ if (my $search = $c->get_param('search')) {
+ $search = $self->trim($search);
+ $search =~ s/^<(.*)>$/$1/; # In case email wrapped in <...>
+ $c->stash->{searched} = $search;
+
+ my $isearch = '%' . $search . '%';
+ my $search_n = 0;
+ $search_n = int($search) if $search =~ /^\d+$/;
+
+ my $users = $c->cobrand->users->search(
+ {
+ -or => [
+ email => { ilike => $isearch },
+ phone => { ilike => $isearch },
+ name => { ilike => $isearch },
+ from_body => $search_n,
+ ]
+ }
+ );
+ my @users = $users->all;
+ $c->stash->{users} = [ @users ];
+ $c->forward('/admin/add_flags', [ { email => { ilike => $isearch } } ]);
+
+ } else {
+ $c->forward('/auth/get_csrf_token');
+ $c->forward('/admin/fetch_all_bodies');
+ $c->cobrand->call_hook('admin_user_edit_extra_data');
+
+
+ # Admin users by default
+ my $users = $c->cobrand->users->search(
+ { from_body => { '!=', undef } },
+ { order_by => 'name' }
+ );
+ my @users = $users->all;
+ $c->stash->{users} = \@users;
+ }
+
+ return 1;
+}
+
+sub add : Local : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'admin/users/edit.html';
+ $c->forward('/auth/get_csrf_token');
+ $c->forward('/admin/fetch_all_bodies');
+ $c->cobrand->call_hook('admin_user_edit_extra_data');
+
+ return unless $c->get_param('submit');
+
+ $c->forward('/auth/check_csrf_token');
+
+ $c->stash->{field_errors} = {};
+ my $email = lc $c->get_param('email');
+ my $phone = $c->get_param('phone');
+ my $email_v = $c->get_param('email_verified');
+ my $phone_v = $c->get_param('phone_verified');
+
+ if ($email && !is_valid_email($email)) {
+ $c->stash->{field_errors}->{email} = _('Please enter a valid email');
+ }
+ unless ($c->get_param('name')) {
+ $c->stash->{field_errors}->{name} = _('Please enter a name');
+ }
+
+ unless ($email || $phone) {
+ $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number');
+ }
+ if (!$email_v && !$phone_v) {
+ $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone');
+ }
+
+ if ($phone_v) {
+ my $parsed_phone = $c->forward('phone_check', [ $phone ]);
+ $phone = $parsed_phone if $parsed_phone;
+ }
+
+ my $existing_email = $email_v && $c->model('DB::User')->find( { email => $email } );
+ my $existing_phone = $phone_v && $c->model('DB::User')->find( { phone => $phone } );
+ if ($existing_email || $existing_phone) {
+ $c->stash->{field_errors}->{username} = _('User already exists');
+ }
+
+ return if %{$c->stash->{field_errors}};
+
+ my $user = $c->model('DB::User')->create( {
+ name => $c->get_param('name'),
+ email => $email ? $email : undef,
+ email_verified => $email && $email_v ? 1 : 0,
+ phone => $phone || undef,
+ phone_verified => $phone && $phone_v ? 1 : 0,
+ from_body => $c->get_param('body') || undef,
+ flagged => $c->get_param('flagged') || 0,
+ # Only superusers can create superusers
+ is_superuser => ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0,
+ } );
+ $c->stash->{user} = $user;
+ $c->forward('user_cobrand_extra_fields');
+ $user->update;
+
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
+
+ $c->flash->{status_message} = _("Updated!");
+ $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) );
+}
+
+sub edit : Path : Args(1) {
+ my ( $self, $c, $id ) = @_;
+
+ $c->forward('/auth/get_csrf_token');
+
+ my $user = $c->cobrand->users->find( { id => $id } );
+ $c->detach( '/page_error_404_not_found', [] ) unless $user;
+
+ unless ( $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) {
+ $c->detach('/page_error_403_access_denied', []);
+ }
+
+ $c->stash->{user} = $user;
+ $c->forward( '/admin/check_username_for_abuse', [ $user ] );
+
+ if ( $user->from_body && $c->user->has_permission_to('user_manage_permissions', $user->from_body->id) ) {
+ $c->stash->{available_permissions} = $c->cobrand->available_permissions;
+ }
+
+ $c->forward('/admin/fetch_all_bodies');
+ $c->forward('/admin/fetch_body_areas', [ $user->from_body ]) if $user->from_body;
+ $c->cobrand->call_hook('admin_user_edit_extra_data');
+
+ if ( defined $c->flash->{status_message} ) {
+ $c->stash->{status_message} =
+ '<p><em>' . $c->flash->{status_message} . '</em></p>';
+ }
+
+ $c->forward('/auth/check_csrf_token') if $c->get_param('submit');
+
+ if ( $c->get_param('submit') and $c->get_param('unban') ) {
+ $c->forward('unban', [ $user ]);
+ } elsif ( $c->get_param('submit') and $c->get_param('logout_everywhere') ) {
+ $c->forward('user_logout_everywhere', [ $user ]);
+ } elsif ( $c->get_param('submit') and $c->get_param('anon_everywhere') ) {
+ $c->forward('user_anon_everywhere', [ $user ]);
+ } elsif ( $c->get_param('submit') and $c->get_param('hide_everywhere') ) {
+ $c->forward('user_hide_everywhere', [ $user ]);
+ } elsif ( $c->get_param('submit') and $c->get_param('remove_account') ) {
+ $c->forward('user_remove_account', [ $user ]);
+ } elsif ( $c->get_param('submit') and $c->get_param('send_login_email') ) {
+ my $email = lc $c->get_param('email');
+ my %args = ( email => $email );
+ $args{user_id} = $id if $user->email ne $email || !$user->email_verified;
+ $c->forward('send_login_email', [ \%args ]);
+ } elsif ( $c->get_param('update_alerts') ) {
+ $c->forward('update_alerts');
+ } elsif ( $c->get_param('submit') ) {
+
+ my $edited = 0;
+
+ my $name = $c->get_param('name');
+ my $email = lc $c->get_param('email');
+ my $phone = $c->get_param('phone');
+ my $email_v = $c->get_param('email_verified') || 0;
+ my $phone_v = $c->get_param('phone_verified') || 0;
+
+ $c->stash->{field_errors} = {};
+
+ unless ($email || $phone) {
+ $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number');
+ }
+ if (!$email_v && !$phone_v) {
+ $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone');
+ }
+ if ($email && !is_valid_email($email)) {
+ $c->stash->{field_errors}->{email} = _('Please enter a valid email');
+ }
+
+ if ($phone_v) {
+ my $parsed_phone = $c->forward('phone_check', [ $phone ]);
+ $phone = $parsed_phone if $parsed_phone;
+ }
+
+ unless ($name) {
+ $c->stash->{field_errors}->{name} = _('Please enter a name');
+ }
+
+ my $email_params = { email => $email, email_verified => 1, id => { '!=', $user->id } };
+ my $phone_params = { phone => $phone, phone_verified => 1, id => { '!=', $user->id } };
+ my $existing_email = $email_v && $c->model('DB::User')->search($email_params)->first;
+ my $existing_phone = $phone_v && $c->model('DB::User')->search($phone_params)->first;
+ my $existing_user = $existing_email || $existing_phone;
+ my $existing_email_cobrand = $email_v && $c->cobrand->users->search($email_params)->first;
+ my $existing_phone_cobrand = $phone_v && $c->cobrand->users->search($phone_params)->first;
+ my $existing_user_cobrand = $existing_email_cobrand || $existing_phone_cobrand;
+ if ($existing_phone_cobrand && $existing_email_cobrand && $existing_email_cobrand->id != $existing_phone_cobrand->id) {
+ $c->stash->{field_errors}->{username} = _('User already exists');
+ }
+
+ return if %{$c->stash->{field_errors}};
+
+ if ( ($user->email || "") ne $email ||
+ $user->name ne $name ||
+ ($user->phone || "") ne $phone ||
+ ($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) ||
+ (!$user->from_body && $c->get_param('body'))
+ ) {
+ $edited = 1;
+ }
+
+ if ($existing_user_cobrand) {
+ $existing_user->adopt($user);
+ $c->forward( '/admin/log_edit', [ $id, 'user', 'merge' ] );
+ return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $existing_user->id ) );
+ }
+
+ $user->email($email) if !$existing_email;
+ $user->phone($phone) if !$existing_phone;
+ $user->email_verified( $email_v );
+ $user->phone_verified( $phone_v );
+ $user->name( $name );
+
+ $user->flagged( $c->get_param('flagged') || 0 );
+ # Only superusers can grant superuser status
+ $user->is_superuser( ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0 );
+ # Superusers can set from_body to any value, but other staff can only
+ # set from_body to the same value as their own from_body.
+ if ( $c->user->is_superuser || $c->cobrand->moniker eq 'zurich' ) {
+ $user->from_body( $c->get_param('body') || undef );
+ } elsif ( $c->user->has_body_permission_to('user_assign_body') ) {
+ if ($c->get_param('body') && $c->get_param('body') eq $c->user->from_body->id ) {
+ $user->from_body( $c->user->from_body );
+ } else {
+ $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} ) {
+ $c->forward('/admin/fetch_body_areas', [ $user->from_body ]);
+ }
+
+ if (!$user->from_body) {
+ # Non-staff users aren't allowed any permissions or to be in an area
+ $user->admin_user_body_permissions->delete;
+ $user->area_ids(undef);
+ delete $c->stash->{areas};
+ delete $c->stash->{fetched_areas_body_id};
+ } elsif ($c->stash->{available_permissions}) {
+ my @all_permissions = map { keys %$_ } values %{ $c->stash->{available_permissions} };
+ my @user_permissions = grep { $c->get_param("permissions[$_]") ? 1 : undef } @all_permissions;
+ $user->admin_user_body_permissions->search({
+ body_id => $user->from_body->id,
+ permission_type => { '!=' => \@user_permissions },
+ })->delete;
+ foreach my $permission_type (@user_permissions) {
+ $user->user_body_permissions->find_or_create({
+ body_id => $user->from_body->id,
+ permission_type => $permission_type,
+ });
+ }
+ }
+
+ if ( $user->from_body && $c->user->has_permission_to('user_assign_areas', $user->from_body->id) ) {
+ my %valid_areas = map { $_->{id} => 1 } @{ $c->stash->{areas} };
+ my @area_ids = grep { $valid_areas{$_} } $c->get_param_list('area_ids');
+ $user->area_ids( @area_ids ? \@area_ids : undef );
+ }
+
+ # Handle 'trusted' flag(s)
+ my @trusted_bodies = $c->get_param_list('trusted_bodies');
+ if ( $c->user->is_superuser ) {
+ $user->user_body_permissions->search({
+ body_id => { '!=' => \@trusted_bodies },
+ permission_type => 'trusted',
+ })->delete;
+ foreach my $body_id (@trusted_bodies) {
+ $user->user_body_permissions->find_or_create({
+ body_id => $body_id,
+ permission_type => 'trusted',
+ });
+ }
+ } elsif ( $c->user->from_body ) {
+ my %trusted = map { $_ => 1 } @trusted_bodies;
+ my $body_id = $c->user->from_body->id;
+ if ( $trusted{$body_id} ) {
+ $user->user_body_permissions->find_or_create({
+ body_id => $body_id,
+ permission_type => 'trusted',
+ });
+ } else {
+ $user->user_body_permissions->search({
+ body_id => $body_id,
+ permission_type => 'trusted',
+ })->delete;
+ }
+ }
+
+ # Update the categories this user operates in
+ if ( $user->from_body ) {
+ $c->stash->{body} = $user->from_body;
+ $c->forward('/admin/fetch_contacts');
+ my @live_contacts = $c->stash->{live_contacts}->all;
+ my @live_contact_ids = map { $_->id } @live_contacts;
+ my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids;
+ $user->set_extra_metadata('categories', \@new_contact_ids);
+ }
+
+ $user->update;
+ if ($edited) {
+ $c->forward( '/admin/log_edit', [ $id, 'user', 'edit' ] );
+ }
+ $c->flash->{status_message} = _("Updated!");
+ return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) );
+ }
+
+ if ( $user->from_body ) {
+ unless ( $c->stash->{live_contacts} ) {
+ $c->stash->{body} = $user->from_body;
+ $c->forward('/admin/fetch_contacts');
+ }
+ my @contacts = @{$user->get_extra_metadata('categories') || []};
+ my %active_contacts = map { $_ => 1 } @contacts;
+ my @live_contacts = $c->stash->{live_contacts}->all;
+ my @all_contacts = map { {
+ id => $_->id,
+ category => $_->category,
+ active => $active_contacts{$_->id},
+ } } @live_contacts;
+ $c->stash->{contacts} = \@all_contacts;
+ }
+
+ # this goes after in case we've delete any alerts
+ unless ( $c->cobrand->moniker eq 'zurich' ) {
+ $c->forward('user_alert_details');
+ }
+
+ return 1;
+}
+
+sub import :Local {
+ my ( $self, $c, $id ) = @_;
+
+ $c->forward('/auth/get_csrf_token');
+ return unless $c->user_exists && $c->user->is_superuser;
+
+ return unless $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 phone_check : Private {
+ my ($self, $c, $phone) = @_;
+ my $parsed = FixMyStreet::SMS->parse_username($phone);
+ if ($parsed->{phone} && $parsed->{may_be_mobile}) {
+ return $parsed->{username};
+ } elsif ($parsed->{phone}) {
+ $c->stash->{field_errors}->{phone} = _('Please enter a mobile number');
+ } else {
+ $c->stash->{field_errors}->{phone} = _('Please check your phone number is correct');
+ }
+}
+
+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 user_alert_details : Private {
+ my ( $self, $c ) = @_;
+
+ my @alerts = $c->stash->{user}->alerts({}, { prefetch => 'alert_type' })->all;
+ $c->stash->{alerts} = \@alerts;
+
+ my @wards;
+
+ for my $alert (@alerts) {
+ if ($alert->alert_type->ref eq 'ward_problems') {
+ push @wards, $alert->parameter2;
+ }
+ }
+
+ if (@wards) {
+ $c->stash->{alert_areas} = FixMyStreet::MapIt::call('areas', join(',', @wards) );
+ }
+
+ my %body_names = map { $_->{id} => $_->{name} } @{ $c->stash->{bodies} };
+ $c->stash->{body_names} = \%body_names;
+}
+
+sub update_alerts : Private {
+ my ($self, $c) = @_;
+
+ my $changes;
+ for my $alert ( $c->stash->{user}->alerts ) {
+ my $edit_option = $c->get_param('edit_alert[' . $alert->id . ']');
+ next unless $edit_option;
+ $changes = 1;
+ if ( $edit_option eq 'delete' ) {
+ $alert->delete;
+ } elsif ( $edit_option eq 'disable' ) {
+ $alert->disable;
+ } elsif ( $edit_option eq 'enable' ) {
+ $alert->confirm;
+ }
+ }
+ $c->flash->{status_message} = _("Updated!") if $changes;
+}
+
+sub user_logout_everywhere : Private {
+ my ( $self, $c, $user ) = @_;
+ my $sessions = $user->get_extra_metadata('sessions');
+ foreach (grep { $_ ne $c->sessionid } @$sessions) {
+ $c->delete_session_data("session:$_");
+ }
+ $c->stash->{status_message} = _('That user has been logged out.');
+}
+
+sub user_anon_everywhere : Private {
+ my ( $self, $c, $user ) = @_;
+ $user->problems->update({anonymous => 1});
+ $user->comments->update({anonymous => 1});
+ $c->stash->{status_message} = _('That user has been made anonymous on all reports and updates.');
+}
+
+sub user_hide_everywhere : Private {
+ my ( $self, $c, $user ) = @_;
+ my $problems = $user->problems->search({ state => { '!=' => 'hidden' } });
+ while (my $problem = $problems->next) {
+ $problem->get_photoset->delete_cached;
+ $problem->update({ state => 'hidden' });
+ }
+ my $updates = $user->comments->search({ state => { '!=' => 'hidden' } });
+ while (my $update = $updates->next) {
+ $update->hide;
+ }
+ $c->stash->{status_message} = _('That user’s reports and updates have been hidden.');
+}
+
+sub send_login_email : Private {
+ my ( $self, $c, $args ) = @_;
+
+ my $token_data = {
+ email => $args->{email},
+ };
+
+ $token_data->{old_user_id} = $args->{user_id} if $args->{user_id};
+ $token_data->{name} = $args->{name} if $args->{name};
+
+ my $token_obj = $c->model('DB::Token')->create({
+ scope => 'email_sign_in',
+ data => $token_data,
+ });
+
+ $c->stash->{token} = $token_obj->token;
+ my $template = 'login.txt';
+
+ # do not use relative URIs in the email, obvs.
+ $c->uri_disposition('absolute');
+ $c->send_email( $template, { to => $args->{email} } );
+
+ $c->stash->{status_message} = _('The user has been sent a login email');
+}
+
+# Anonymize and remove name from all problems/updates, disable all alerts.
+# Remove their account's email address, phone number, password, etc.
+sub user_remove_account : Private {
+ my ( $self, $c, $user ) = @_;
+ $c->forward('user_logout_everywhere', [ $user ]);
+ $user->anonymize_account;
+ $c->stash->{status_message} = _('That user’s personal details have been removed.');
+}
+
+=head2 ban
+
+Add the user's email address/phone number to the abuse table if they are not
+already in there and sets status_message accordingly.
+
+=cut
+
+sub ban : Private {
+ my ( $self, $c ) = @_;
+
+ my $user;
+ if ($c->stash->{problem}) {
+ $user = $c->stash->{problem}->user;
+ } elsif ($c->stash->{update}) {
+ $user = $c->stash->{update}->user;
+ }
+ return unless $user;
+
+ if ($user->email_verified && $user->email) {
+ my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->email });
+ if ( $abuse->in_storage ) {
+ $c->stash->{status_message} = _('User already in abuse list');
+ } else {
+ $abuse->insert;
+ $c->stash->{status_message} = _('User added to abuse list');
+ }
+ $c->stash->{username_in_abuse} = 1;
+ }
+ if ($user->phone_verified && $user->phone) {
+ my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->phone });
+ if ( $abuse->in_storage ) {
+ $c->stash->{status_message} = _('User already in abuse list');
+ } else {
+ $abuse->insert;
+ $c->stash->{status_message} = _('User added to abuse list');
+ }
+ $c->stash->{username_in_abuse} = 1;
+ }
+ return 1;
+}
+
+sub unban : Private {
+ my ( $self, $c, $user ) = @_;
+
+ my @username;
+ if ($user->email_verified && $user->email) {
+ push @username, $user->email;
+ }
+ if ($user->phone_verified && $user->phone) {
+ push @username, $user->phone;
+ }
+ if (@username) {
+ my $abuse = $c->model('DB::Abuse')->search({ email => \@username });
+ if ( $abuse ) {
+ $abuse->delete;
+ $c->stash->{status_message} = _('user removed from abuse list');
+ } else {
+ $c->stash->{status_message} = _('user not in abuse list');
+ }
+ $c->stash->{username_in_abuse} = 0;
+ }
+}
+
+=head2 flag
+
+Sets the flag on a user
+
+=cut
+
+sub flag : Private {
+ my ( $self, $c ) = @_;
+
+ my $user;
+ if ($c->stash->{problem}) {
+ $user = $c->stash->{problem}->user;
+ } elsif ($c->stash->{update}) {
+ $user = $c->stash->{update}->user;
+ }
+
+ if ( !$user ) {
+ $c->stash->{status_message} = _('Could not find user');
+ } else {
+ $user->flagged(1);
+ $user->update;
+ $c->stash->{status_message} = _('User flagged');
+ }
+
+ $c->stash->{user_flagged} = 1;
+
+ return 1;
+}
+
+=head2 flag_remove
+
+Remove the flag on a user
+
+=cut
+
+sub flag_remove : Private {
+ my ( $self, $c ) = @_;
+
+ my $user;
+ if ($c->stash->{problem}) {
+ $user = $c->stash->{problem}->user;
+ } elsif ($c->stash->{update}) {
+ $user = $c->stash->{update}->user;
+ }
+
+ if ( !$user ) {
+ $c->stash->{status_message} = _('Could not find user');
+ } else {
+ $user->flagged(0);
+ $user->update;
+ $c->stash->{status_message} = _('User flag removed');
+ }
+
+ return 1;
+}
+
+
+sub trim {
+ my $self = shift;
+ my $e = shift;
+ $e =~ s/^\s+//;
+ $e =~ s/\s+$//;
+ return $e;
+}
+
+=head1 AUTHOR
+
+mySociety
+
+=head1 LICENSE
+
+This library is free software. You can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm
index 8fed5c3aa..a09161494 100644
--- a/perllib/FixMyStreet/App/Controller/Around.pm
+++ b/perllib/FixMyStreet/App/Controller/Around.pm
@@ -9,6 +9,7 @@ use Encode;
use JSON::MaybeXS;
use Utils;
use Try::Tiny;
+use Text::CSV;
=head1 NAME
@@ -54,6 +55,9 @@ sub index : Path : Args(0) {
|| $c->forward('/location/determine_location_from_pc');
unless ($ret) {
return $c->res->redirect('/') unless $c->get_param('pc') || $partial_report;
+ # Cobrand may want to perform custom searching at this point,
+ # e.g. presenting a list of reports matching the user's query.
+ $c->cobrand->call_hook("around_custom_search");
return;
}
@@ -227,6 +231,10 @@ sub check_and_stash_category : Private {
my $all_areas = $c->stash->{all_areas};
my @bodies = $c->model('DB::Body')->active->for_areas(keys %$all_areas)->all;
my %bodies = map { $_->id => $_ } @bodies;
+ my @list_of_names = map { $_->name } values %bodies;
+ my $csv = Text::CSV->new();
+ $csv->combine(@list_of_names);
+ $c->{stash}->{list_of_names_as_string} = $csv->string;
my @categories = $c->model('DB::Contact')->not_deleted->search(
{
@@ -250,16 +258,18 @@ sub map_features : Private {
my ($self, $c, $extra) = @_;
$c->stash->{page} = 'around'; # Needed by _item.html / so the map knows to make clickable pins, update on pan
+ $c->stash->{num_old_reports} = 0;
$c->forward( '/reports/stash_report_filter_status' );
$c->forward( '/reports/stash_report_sort', [ 'created-desc' ]);
+ $c->stash->{show_old_reports} = $c->get_param('show_old_reports');
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 ) =
+ my ( $on_map, $nearby ) =
FixMyStreet::Map::map_features(
$c, %$extra,
categories => [ keys %{$c->stash->{filter_category}} ],
@@ -280,7 +290,6 @@ sub map_features : Private {
$c->stash->{pins} = \@pins;
$c->stash->{on_map} = $on_map;
$c->stash->{around_map} = $nearby;
- $c->stash->{distance} = $distance;
}
=head2 ajax
@@ -308,6 +317,18 @@ sub ajax : Path('/ajax') {
$c->forward('/reports/ajax', [ 'around/on_map_list_items.html' ]);
}
+sub nearby : Path {
+ my ($self, $c) = @_;
+
+ my $states = FixMyStreet::DB::Result::Problem->open_states();
+ $c->forward('/report/_nearby_json', [ {
+ latitude => $c->get_param('latitude'),
+ longitude => $c->get_param('longitude'),
+ categories => [ $c->get_param('filter_category') || () ],
+ states => $states,
+ } ]);
+}
+
sub location_closest_address : Path('/ajax/closest') {
my ( $self, $c ) = @_;
$c->res->content_type('application/json; charset=utf-8');
@@ -389,10 +410,13 @@ sub _geocode : Private {
sub lookup_by_ref : Private {
my ( $self, $c, $ref ) = @_;
- my $problems = $c->cobrand->problems->search([
- id => $ref,
- external_id => $ref
- ]);
+ my $criteria = $c->cobrand->call_hook("lookup_by_ref", $ref) ||
+ [
+ id => $ref,
+ external_id => $ref
+ ];
+
+ my $problems = $c->cobrand->problems->search( $criteria );
my $count = try {
$problems->count;
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index 533e6a9be..c194045b9 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -54,9 +54,18 @@ sub general : Path : Args(0) {
}
-sub general_test : Path('_test_') : Args(0) {
+sub create : Path('create') : Args(0) {
my ( $self, $c ) = @_;
- $c->stash->{template} = 'auth/token.html';
+ return unless $c->req->method eq 'POST';
+ $c->detach('code_sign_in');
+}
+
+sub forgot : Path('forgot') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{forgotten} = 1;
+ $c->stash->{template} = 'auth/create.html';
+ return unless $c->req->method eq 'POST';
+ $c->detach('code_sign_in');
}
sub authenticate : Private {
@@ -77,7 +86,6 @@ sub sign_in : Private {
$username ||= '';
my $password = $c->get_param('password_sign_in') || '';
- my $remember_me = $c->get_param('remember_me') || 0;
# Sign out just in case
$c->logout();
@@ -91,10 +99,6 @@ sub sign_in : Private {
$c->user->update({ password => $password });
}
- # unless user asked to be remembered limit the session to browser
- $c->set_session_cookie_expire(0)
- unless $remember_me;
-
# Regenerate CSRF token as session ID changed
$c->forward('get_csrf_token');
@@ -104,7 +108,6 @@ sub sign_in : Private {
$c->stash(
sign_in_error => 1,
username => $username,
- remember_me => $remember_me,
);
return;
}
@@ -224,7 +227,8 @@ sub token : Path('/M') : Args(1) {
my $data = $c->forward('get_token', [ $url_token, 'email_sign_in' ]) || return;
$c->stash->{token_not_found} = 1, return
- if $data->{old_user_id} && (!$c->user_exists || $c->user->id ne $data->{old_user_id});
+ if $data->{old_user_id} && $data->{r} && $data->{r} eq 'auth/change_email/success'
+ && (!$c->user_exists || $c->user->id ne $data->{old_user_id});
my $type = $data->{login_type} || 'email';
$c->detach( '/auth/process_login', [ $data, $type ] );
@@ -314,7 +318,7 @@ categories this user has been assigned to.
sub redirect_to_categories : Private {
my ( $self, $c ) = @_;
- my $categories = join(',', @{ $c->user->categories });
+ my $categories = $c->user->categories_string;
my $body_short = $c->cobrand->short_name( $c->user->from_body );
$c->res->redirect( $c->uri_for( "/reports/" . $body_short, { filter_category => $categories } ) );
diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm
index 997009b87..fb525fc1f 100644
--- a/perllib/FixMyStreet/App/Controller/Contact.pm
+++ b/perllib/FixMyStreet/App/Controller/Contact.pm
@@ -4,6 +4,7 @@ use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
+use MIME::Base64;
use mySociety::EmailUtil;
use FixMyStreet::Email;
@@ -17,8 +18,19 @@ Contact us page
=head1 METHODS
+=head2 auto
+
+Functions to run on both GET and POST contact requests.
+
=cut
+sub auto : Private {
+ my ($self, $c) = @_;
+ $c->forward('setup_request');
+ $c->forward('determine_contact_type');
+ $c->forward('/auth/get_csrf_token');
+}
+
=head2 index
Display contact us page
@@ -27,10 +39,6 @@ Display contact us page
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
-
- return
- unless $c->forward('setup_request')
- && $c->forward('determine_contact_type');
}
=head2 submit
@@ -42,20 +50,12 @@ Handle contact us form submission
sub submit : Path('submit') : Args(0) {
my ( $self, $c ) = @_;
- if (my $testing = $c->get_param('_test_')) {
- $c->stash->{success} = $c->get_param('success');
- return;
- }
-
$c->res->redirect( '/contact' ) and return unless $c->req->method eq 'POST';
- return
- unless $c->forward('setup_request')
- && $c->forward('determine_contact_type')
- && $c->forward('validate')
- && $c->forward('prepare_params_for_email')
- && $c->forward('send_email')
- && $c->forward('redirect_on_success');
+ $c->go('index') unless $c->forward('validate');
+ $c->forward('prepare_params_for_email');
+ $c->forward('send_email');
+ $c->forward('redirect_on_success');
}
=head2 determine_contact_type
@@ -105,6 +105,8 @@ sub determine_contact_type : Private {
if ( $c->get_param("reject") && $c->user->has_permission_to(report_reject => $c->stash->{problem}->bodies_str_ids) ) {
$c->stash->{rejecting_report} = 1;
}
+ } elsif ( $c->cobrand->abuse_reports_only ) {
+ $c->detach( '/page_error_404_not_found' );
}
return 1;
@@ -120,6 +122,10 @@ to index page if errors.
sub validate : Private {
my ( $self, $c ) = @_;
+ $c->forward('/auth/check_csrf_token');
+ my $s = $c->stash->{s} = unpack("N", decode_base64($c->get_param('s')));
+ return if !FixMyStreet->test_mode && time() < $s; # uncoverable statement
+
my ( %field_errors, @errors );
my %required = (
name => _('Please enter your name'),
@@ -157,7 +163,7 @@ sub validate : Private {
if ( @errors or scalar keys %field_errors ) {
$c->stash->{errors} = \@errors;
$c->stash->{field_errors} = \%field_errors;
- $c->go('index');
+ return 0;
}
return 1;
@@ -233,6 +239,10 @@ sub setup_request : Private {
# name is already used in the stash for the app class name
$c->stash->{form_name} = $c->get_param('name');
+ my $s = encode_base64(pack("N", time() + 10), '');
+ $s =~ s/=+$//;
+ $c->stash->{s} = $s;
+
return 1;
}
@@ -262,6 +272,7 @@ sub send_email : Private {
my $from = [ $c->stash->{em}, $c->stash->{form_name} ];
my $params = {
to => [ [ $recipient, _($recipient_name) ] ],
+ user_agent => $c->req->user_agent,
};
if (FixMyStreet::Email::test_dmarc($c->stash->{em})) {
$params->{'Reply-To'} = [ $from ];
diff --git a/perllib/FixMyStreet/App/Controller/Council.pm b/perllib/FixMyStreet/App/Controller/Council.pm
index 2e2dce0f7..4acaba903 100644
--- a/perllib/FixMyStreet/App/Controller/Council.pm
+++ b/perllib/FixMyStreet/App/Controller/Council.pm
@@ -2,6 +2,8 @@ package FixMyStreet::App::Controller::Council;
use Moose;
use namespace::autoclean;
+use FixMyStreet::MapIt;
+
BEGIN {extends 'Catalyst::Controller'; }
=head1 NAME
@@ -59,10 +61,6 @@ sub load_and_check_areas : Private {
my $all_areas;
- my %params;
- $params{generation} = $c->config->{MAPIT_GENERATION}
- if $c->config->{MAPIT_GENERATION};
-
if ($prefetched_all_areas) {
$all_areas = {
map { $_ => { id => $_ } }
@@ -71,8 +69,7 @@ sub load_and_check_areas : Private {
} elsif ( $c->stash->{fetch_all_areas} ) {
my %area_types = map { $_ => 1 } @$area_types;
$all_areas =
- mySociety::MaPit::call( 'point',
- "4326/$longitude,$latitude", %params );
+ FixMyStreet::MapIt::call('point', "4326/$longitude,$latitude");
$c->stash->{all_areas_mapit} = $all_areas;
$all_areas = {
map { $_ => $all_areas->{$_} }
@@ -81,9 +78,7 @@ sub load_and_check_areas : Private {
};
} else {
$all_areas =
- mySociety::MaPit::call( 'point',
- "4326/$longitude,$latitude", %params,
- type => $area_types );
+ FixMyStreet::MapIt::call('point', "4326/$longitude,$latitude", type => $area_types);
}
if ($all_areas->{error}) {
$c->stash->{location_error_mapit_error} = 1;
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index 790e7ec29..bd60f8570 100644
--- a/perllib/FixMyStreet/App/Controller/Dashboard.pm
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -3,10 +3,12 @@ use Moose;
use namespace::autoclean;
use DateTime;
+use Encode;
use JSON::MaybeXS;
use Path::Tiny;
use Text::CSV;
use Time::Piece;
+use FixMyStreet::DateRange;
BEGIN { extends 'Catalyst::Controller'; }
@@ -54,6 +56,18 @@ Checks if we can view this page, and if not redirect to 404.
sub check_page_allowed : Private {
my ( $self, $c ) = @_;
+ # dashboard_permission can return undef (if not present, or to carry on
+ # with default behaviour), a body ID to use that body for results, or 0
+ # to refuse access entirely
+ my $cobrand_check = $c->cobrand->call_hook('dashboard_permission');
+ if (defined $cobrand_check) {
+ if ($cobrand_check) {
+ $cobrand_check = $c->model('DB::Body')->find({ id => $cobrand_check });
+ }
+ $c->detach( '/page_error_404_not_found' ) if !$cobrand_check;
+ return $cobrand_check;
+ }
+
$c->detach( '/auth/redirect' ) unless $c->user_exists;
$c->detach( '/page_error_404_not_found' )
@@ -93,13 +107,18 @@ sub index : Path : Args(0) {
# See if we've had anything from the body dropdowns
$c->stash->{category} = $c->get_param('category');
- $c->stash->{ward} = $c->get_param('ward');
- if ($c->user->area_id) {
- $c->stash->{ward} = $c->user->area_id;
- $c->stash->{body_name} = join "", map { $children->{$_}->{name} } grep { $children->{$_} } $c->user->area_id;
+ $c->stash->{ward} = [ $c->get_param_list('ward') ];
+ if ($c->user_exists) {
+ if (my @areas = @{$c->user->area_ids || []}) {
+ $c->stash->{ward} = $c->user->area_ids;
+ $c->stash->{body_name} = join " / ", sort map { $children->{$_}->{name} } grep { $children->{$_} } @areas;
+ }
}
} else {
- my @bodies = $c->model('DB::Body')->active->translated->with_area_count->all_sorted;
+ my @bodies = $c->model('DB::Body')->search(undef, {
+ columns => [ "id", "name" ],
+ })->active->translated->with_area_count->all_sorted;
+ $c->stash->{ward} = [];
$c->stash->{bodies} = \@bodies;
}
@@ -110,10 +129,14 @@ sub index : Path : Args(0) {
$c->stash->{end_date} = $c->get_param('end_date');
$c->stash->{q_state} = $c->get_param('state') || '';
- $c->forward('construct_rs_filter');
+ $c->forward('construct_rs_filter', [ $c->get_param('updates') ]);
if ( $c->get_param('export') ) {
- $c->forward('export_as_csv');
+ if ($c->get_param('updates')) {
+ $c->forward('export_as_csv_updates');
+ } else {
+ $c->forward('export_as_csv');
+ }
} else {
$c->forward('generate_grouped_data');
$self->generate_summary_figures($c);
@@ -121,36 +144,39 @@ sub index : Path : Args(0) {
}
sub construct_rs_filter : Private {
- my ($self, $c) = @_;
+ my ($self, $c, $updates) = @_;
my %where;
- $where{areas} = { 'like', '%,' . $c->stash->{ward} . ',%' }
- if $c->stash->{ward};
+ $where{areas} = [ map { { 'like', "%,$_,%" } } @{$c->stash->{ward}} ]
+ if @{$c->stash->{ward}};
$where{category} = $c->stash->{category}
if $c->stash->{category};
+ my $table_name = $updates ? 'problem' : 'me';
+
my $state = $c->stash->{q_state};
if ( FixMyStreet::DB::Result::Problem->fixed_states->{$state} ) { # Probably fixed - council
- $where{'me.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
+ $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
} elsif ( $state ) {
- $where{'me.state'} = $state;
+ $where{"$table_name.state"} = $state;
} else {
- $where{'me.state'} = [ FixMyStreet::DB::Result::Problem->visible_states() ];
+ $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->visible_states() ];
}
- my $dtf = $c->model('DB')->storage->datetime_parser;
-
- my $start_date = $dtf->parse_datetime($c->stash->{start_date});
- $where{'me.confirmed'} = { '>=', $dtf->format_datetime($start_date) };
+ my $days30 = DateTime->now(time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone)->subtract(days => 30);
+ $days30->truncate( to => 'day' );
- 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) } ];
- }
+ my $range = FixMyStreet::DateRange->new(
+ start_date => $c->stash->{start_date},
+ start_default => $days30,
+ end_date => $c->stash->{end_date},
+ formatter => $c->model('DB')->storage->datetime_parser,
+ );
+ $where{"$table_name.confirmed"} = $range->sql;
$c->stash->{params} = \%where;
- $c->stash->{problems_rs} = $c->cobrand->problems->to_body($c->stash->{body})->search( \%where );
+ my $rs = $updates ? $c->cobrand->updates : $c->cobrand->problems;
+ $c->stash->{objects_rs} = $rs->to_body($c->stash->{body})->search( \%where );
}
sub generate_grouped_data : Private {
@@ -182,7 +208,7 @@ sub generate_grouped_data : Private {
@groups = qw/category state/;
%grouped = map { $_->category => {} } @{$c->stash->{contacts}};
}
- my $problems = $c->stash->{problems_rs}->search(undef, {
+ my $problems = $c->stash->{objects_rs}->search(undef, {
group_by => [ map { ref $_ ? $_->{-as} : $_ } @groups ],
select => [ @groups, { count => 'me.id' } ],
as => [ @groups == 2 ? qw/key1 key2 count/ : qw/key1 count/ ],
@@ -238,7 +264,7 @@ sub generate_summary_figures {
# problems this month by state
$c->stash->{"summary_$_"} = 0 for values %$state_map;
- $c->stash->{summary_open} = $c->stash->{problems_rs}->count;
+ $c->stash->{summary_open} = $c->stash->{objects_rs}->count;
my $params = $c->stash->{params};
$params = { map { my $n = $_; s/me\./problem\./ unless /me\.confirmed/; $_ => $params->{$n} } keys %$params };
@@ -268,15 +294,54 @@ sub generate_summary_figures {
sub generate_body_response_time : Private {
my ( $self, $c ) = @_;
- my $avg = $c->stash->{body}->calculate_average;
+ my $avg = $c->stash->{body}->calculate_average($c->cobrand->call_hook("body_responsiveness_threshold"));
$c->stash->{body_average} = $avg ? int($avg / 60 / 60 / 24 + 0.5) : 0;
}
+sub csv_filename {
+ my ($self, $c, $updates) = @_;
+ my %where = (
+ category => $c->stash->{category},
+ state => $c->stash->{q_state},
+ ward => join(',', @{$c->stash->{ward}}),
+ );
+ $where{body} = $c->stash->{body}->id if $c->stash->{body};
+ join '-',
+ $c->req->uri->host,
+ $updates ? ('updates') : (),
+ map {
+ my $value = $where{$_};
+ (defined $value and length $value) ? ($_, $value) : ()
+ } sort keys %where
+};
+
+sub export_as_csv_updates : Private {
+ my ($self, $c) = @_;
+
+ my $csv = $c->stash->{csv} = {
+ objects => $c->stash->{objects_rs}->search_rs({}, {
+ order_by => ['me.confirmed', 'me.id'],
+ '+columns' => ['problem.bodies_str'],
+ }),
+ headers => [
+ 'Report ID', 'Update ID', 'Date', 'Status', 'Problem state',
+ 'Text', 'User Name', 'Reported As',
+ ],
+ columns => [
+ 'problem_id', 'id', 'confirmed', 'state', 'problem_state',
+ 'text', 'user_name_display', 'reported_as',
+ ],
+ filename => $self->csv_filename($c, 1),
+ };
+ $c->cobrand->call_hook("dashboard_export_updates_add_columns");
+ $c->forward('generate_csv');
+}
+
sub export_as_csv : Private {
my ($self, $c) = @_;
my $csv = $c->stash->{csv} = {
- problems => $c->stash->{problems_rs}->search_rs({}, {
+ objects => $c->stash->{objects_rs}->search_rs({}, {
prefetch => 'comments',
order_by => ['me.confirmed', 'me.id'],
}),
@@ -298,6 +363,8 @@ sub export_as_csv : Private {
'Easting',
'Northing',
'Report URL',
+ 'Site Used',
+ 'Reported As',
],
columns => [
'id',
@@ -317,23 +384,12 @@ sub export_as_csv : Private {
'local_coords_x',
'local_coords_y',
'url',
+ 'site_used',
+ 'reported_as',
],
- filename => do {
- my %where = (
- category => $c->stash->{category},
- state => $c->stash->{q_state},
- ward => $c->stash->{ward},
- );
- $where{body} = $c->stash->{body}->id if $c->stash->{body};
- join '-',
- $c->req->uri->host,
- map {
- my $value = $where{$_};
- (defined $value and length $value) ? ($_, $value) : ()
- } sort keys %where
- },
+ filename => $self->csv_filename($c, 0),
};
- $c->cobrand->call_hook("dashboard_export_add_columns");
+ $c->cobrand->call_hook("dashboard_export_problems_add_columns");
$c->forward('generate_csv');
}
@@ -354,24 +410,44 @@ hashref of extra data to include that can be used by 'columns'.
sub generate_csv : Private {
my ($self, $c) = @_;
+ my $filename = $c->stash->{csv}->{filename};
+ $c->res->content_type('text/csv; charset=utf-8');
+ $c->res->header('content-disposition' => "attachment; filename=\"${filename}.csv\"");
+
+ # Emit a header (copying Drupal's naming) telling an intermediary (e.g.
+ # Varnish) not to buffer the output. Varnish will need to know this, e.g.:
+ # if (beresp.http.Surrogate-Control ~ "BigPipe/1.0") {
+ # set beresp.do_stream = true;
+ # set beresp.ttl = 0s;
+ # }
+ $c->res->header('Surrogate-Control' => 'content="BigPipe/1.0"');
+
+ # Tell nginx not to buffer this response
+ $c->res->header('X-Accel-Buffering' => 'no');
+
+ # Define an empty body so the web view doesn't get added at the end
+ $c->res->body("");
+
+ # Old parameter renaming
+ $c->stash->{csv}->{objects} //= $c->stash->{csv}->{problems};
+
my $csv = Text::CSV->new({ binary => 1, eol => "\n" });
- $csv->combine(@{$c->stash->{csv}->{headers}});
- my @body = ($csv->string);
+ $csv->print($c->response, $c->stash->{csv}->{headers});
my $fixed_states = FixMyStreet::DB::Result::Problem->fixed_states;
my $closed_states = FixMyStreet::DB::Result::Problem->closed_states;
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, \%asked_for);
+ my $objects = $c->stash->{csv}->{objects};
+ while ( my $obj = $objects->next ) {
+ my $hashref = $obj->as_hashref($c, \%asked_for);
- $hashref->{user_name_display} = $report->anonymous
- ? '(anonymous)' : $report->name;
+ $hashref->{user_name_display} = $obj->anonymous
+ ? '(anonymous)' : $obj->name;
if ($asked_for{acknowledged}) {
- for my $comment ($report->comments) {
+ for my $comment ($obj->comments) {
my $problem_state = $comment->problem_state or next;
next unless $comment->state eq 'confirmed';
next if $problem_state eq 'confirmed';
@@ -392,28 +468,33 @@ sub generate_csv : Private {
split ',', $hashref->{areas};
}
- ($hashref->{local_coords_x}, $hashref->{local_coords_y}) =
- $report->local_coords;
- $hashref->{url} = join '', $c->cobrand->base_url_for_report($report), $report->url;
+ if ($obj->can('local_coords') && $asked_for{local_coords_x}) {
+ ($hashref->{local_coords_x}, $hashref->{local_coords_y}) =
+ $obj->local_coords;
+ }
+ if ($obj->can('url')) {
+ my $base = $c->cobrand->base_url_for_report($obj->can('problem') ? $obj->problem : $obj);
+ $hashref->{url} = join '', $base, $obj->url;
+ }
+
+ $hashref->{site_used} = $obj->can('service') ? ($obj->service || $obj->cobrand) : $obj->cobrand;
+
+ $hashref->{reported_as} = $obj->get_extra_metadata('contributed_as') || '';
if (my $fn = $c->stash->{csv}->{extra_data}) {
- my $extra = $fn->($report);
+ my $extra = $fn->($obj);
$hashref = { %$hashref, %$extra };
}
- $csv->combine(
+ $csv->print($c->response, [
+ map {
+ $_ = encode('UTF-8', $_) if $_;
+ }
@{$hashref}{
@{$c->stash->{csv}->{columns}}
},
- );
-
- push @body, $csv->string;
+ ] );
}
-
- my $filename = $c->stash->{csv}->{filename};
- $c->res->content_type('text/csv; charset=utf-8');
- $c->res->header('content-disposition' => "attachment; filename=${filename}.csv");
- $c->res->body( join "", @body );
}
=head1 AUTHOR
diff --git a/perllib/FixMyStreet/App/Controller/Develop.pm b/perllib/FixMyStreet/App/Controller/Develop.pm
index 0bc52883f..ae7122fa1 100755
--- a/perllib/FixMyStreet/App/Controller/Develop.pm
+++ b/perllib/FixMyStreet/App/Controller/Develop.pm
@@ -26,10 +26,21 @@ Makes sure this controller is only available when run in development.
sub auto : Private {
my ($self, $c) = @_;
- $c->detach( '/page_error_404_not_found' ) unless $c->config->{STAGING_SITE};
+ $c->detach( '/page_error_404_not_found' ) unless $c->user_exists && $c->user->is_superuser;
return 1;
}
+=item index
+
+Shows a list of links to preview HTML emails.
+
+=cut
+
+sub index : Path('/_dev') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{problem} = $c->model('DB::Problem')->search(undef, { rows => 1 } )->first;
+}
+
=item email_list
Shows a list of links to preview HTML emails.
@@ -49,10 +60,11 @@ sub email_list : Path('/_dev/email') : Args(0) {
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,
+ 'confirm_report_sent' => 1,
'problem-moderated' => 1, 'questionnaire' => 1, 'submit' => 1);
- my $update = $c->model('DB::Comment')->first;
- my $problem = $c->model('DB::Problem')->first;
+ my $update = $c->model('DB::Comment')->search(undef, { rows => 1 } )->first;
+ my $problem = $c->model('DB::Problem')->search(undef, { rows => 1 } )->first;
$c->stash->{templates} = [];
foreach (sort keys %templates) {
@@ -130,6 +142,115 @@ sub email_previewer : Path('/_dev/email') : Args(1) {
$c->response->body($html);
}
+=item problem_confirm_previewer
+
+Displays the confirmation page for a given problem.
+
+=back
+
+=cut
+
+sub problem_confirm_previewer : Path('/_dev/confirm_problem') : Args(1) {
+ my ( $self, $c, $id ) = @_;
+
+ $c->log->info('Previewing confirmation page for problem ' . $id);
+
+ my $problem = $c->model('DB::Problem')->find( { id => $id } )
+ || $c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] );
+ $c->stash->{report} = $problem;
+
+ $c->log->info('Problem ' . $id . ' found: ' . $problem->title);
+ $c->stash->{template} = 'tokens/confirm_problem.html';
+}
+
+=item update_confirm_previewer
+
+Displays the confirmation page for an update on the given problem.
+
+=back
+
+=cut
+
+sub update_confirm_previewer : Path('/_dev/confirm_update') : Args(1) {
+ my ( $self, $c, $id ) = @_;
+
+ my $problem = $c->model('DB::Problem')->find( { id => $id } )
+ || $c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] );
+ $c->stash->{problem} = $problem;
+
+ $c->stash->{template} = 'tokens/confirm_update.html';
+}
+
+=item alert_confirm_previewer
+
+Displays the confirmation page for an alert, with the supplied
+confirmation type (ie: subscribed, or unsubscribed).
+
+=back
+
+=cut
+
+sub alert_confirm_previewer : Path('/_dev/confirm_alert') : Args(1) {
+ my ( $self, $c, $confirm_type ) = @_;
+ $c->stash->{confirm_type} = $confirm_type;
+ $c->stash->{template} = 'tokens/confirm_alert.html';
+}
+
+=item contact_submit_previewer
+
+Displays the contact submission page, with success based on the
+truthyness of the supplied argument.
+
+=back
+
+=cut
+
+sub contact_submit_previewer : Path('/_dev/contact_submit') : Args(1) {
+ my ( $self, $c, $success ) = @_;
+ $c->stash->{success} = $success;
+ $c->stash->{template} = 'contact/submit.html';
+}
+
+=item questionnaire_completed_previewer
+
+Displays the questionnaire completed page, with content based on
+the supplied ?new_state and ?been_fixed query params.
+
+=back
+
+=cut
+
+sub questionnaire_completed_previewer : Path('/_dev/questionnaire_completed') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{been_fixed} = $c->get_param('been_fixed');
+ $c->stash->{new_state} = $c->get_param('new_state');
+ $c->stash->{template} = 'questionnaire/completed.html';
+}
+
+=item questionnaire_creator_fixed_previewer
+
+Displays the page a user sees after they mark their own report as fixed.
+
+=back
+
+=cut
+
+sub questionnaire_creator_fixed_previewer : Path('/_dev/questionnaire_creator_fixed') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'questionnaire/creator_fixed.html';
+}
+
+sub auth_preview : Path('/_dev/auth') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'auth/token.html';
+}
+
+sub report_new_preview : Path('/_dev/report_new') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'email_sent.html';
+ $c->stash->{email_type} = $c->get_param('email_type');
+}
+
__PACKAGE__->meta->make_immutable;
1;
diff --git a/perllib/FixMyStreet/App/Controller/JS.pm b/perllib/FixMyStreet/App/Controller/JS.pm
index 1ced9d43b..f430ce672 100755
--- a/perllib/FixMyStreet/App/Controller/JS.pm
+++ b/perllib/FixMyStreet/App/Controller/JS.pm
@@ -20,7 +20,9 @@ of translation strings.
sub translation_strings : LocalRegex('^translation_strings\.(.*?)\.js$') : Args(0) {
my ( $self, $c ) = @_;
my $lang = $c->req->captures->[0];
- $c->cobrand->set_lang_and_domain( $lang, 1 );
+ $c->cobrand->set_lang_and_domain( $lang, 1,
+ FixMyStreet->path_to('locale')->stringify
+ );
$c->res->content_type( 'application/javascript' );
}
diff --git a/perllib/FixMyStreet/App/Controller/JSON.pm b/perllib/FixMyStreet/App/Controller/JSON.pm
index 762e3c115..ccc5b31dc 100644
--- a/perllib/FixMyStreet/App/Controller/JSON.pm
+++ b/perllib/FixMyStreet/App/Controller/JSON.pm
@@ -8,6 +8,7 @@ use JSON::MaybeXS;
use DateTime;
use DateTime::Format::ISO8601;
use List::MoreUtils 'uniq';
+use FixMyStreet::DateRange;
=head1 NAME
@@ -50,16 +51,19 @@ sub problems : Local {
}
# convert the dates to datetimes and trap errors
- my $iso8601 = DateTime::Format::ISO8601->new;
- my $start_dt = eval { $iso8601->parse_datetime($start_date); };
- my $end_dt = eval { $iso8601->parse_datetime($end_date); };
- unless ( $start_dt && $end_dt ) {
+ my $range = FixMyStreet::DateRange->new(
+ start_date => $start_date,
+ end_date => $end_date,
+ parser => DateTime::Format::ISO8601->new,
+ formatter => $c->model('DB')->schema->storage->datetime_parser,
+ );
+ unless ($range->start && $range->end) {
$c->stash->{error} = 'Invalid dates supplied';
return;
}
# check that the dates are sane
- if ( $start_dt > $end_dt ) {
+ if ($range->start >= $range->end) {
$c->stash->{error} = 'Start date after end date';
return;
}
@@ -80,15 +84,10 @@ sub problems : Local {
$date_col = 'lastupdate';
}
- my $dt_parser = $c->model('DB')->schema->storage->datetime_parser;
-
- my $one_day = DateTime::Duration->new( days => 1 );
my $query = {
- $date_col => {
- '>=' => $dt_parser->format_datetime($start_dt),
- '<=' => $dt_parser->format_datetime($end_dt + $one_day),
- },
+ $date_col => $range->sql,
state => [ @state ],
+ non_public => 0,
};
$query->{category} = $category if $category;
my @problems = $c->cobrand->problems->search( $query, {
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index 86143b5ea..22869d531 100644
--- a/perllib/FixMyStreet/App/Controller/Moderate.pm
+++ b/perllib/FixMyStreet/App/Controller/Moderate.pm
@@ -23,9 +23,9 @@ data to change.
- user to be from_body
- user to have a "moderate" record in user_body_permissions
-The original data of the report is stored in moderation_original_data, so
-that it can be reverted/consulted if required. All moderation events are
-stored in admin_log.
+The original and previous data of the report is stored in
+moderation_original_data, so that it can be reverted/consulted if required.
+All moderation events are stored in admin_log.
=head1 SEE ALSO
@@ -37,71 +37,155 @@ DB tables:
=cut
+sub end : ActionClass('RenderView') {
+ my ($self, $c) = @_;
+
+ if ($c->stash->{moderate_errors}) {
+ $c->stash->{show_moderation} = 'report';
+ $c->stash->{template} = 'report/display.html';
+ $c->forward('/report/display');
+ } elsif ($c->res->redirect) {
+ # Do nothing if we're already going somewhere
+ } else {
+ $c->res->redirect($c->stash->{report_uri});
+ }
+}
+
sub moderate : Chained('/') : PathPart('moderate') : CaptureArgs(0) { }
sub report : Chained('moderate') : PathPart('report') : CaptureArgs(1) {
my ($self, $c, $id) = @_;
my $problem = $c->model('DB::Problem')->find($id);
+ $c->detach unless $problem;
my $cobrand_base = $c->cobrand->base_url_for_report( $problem );
my $report_uri = $cobrand_base . $problem->url;
$c->stash->{cobrand_base} = $cobrand_base;
$c->stash->{report_uri} = $report_uri;
- $c->res->redirect( $report_uri ); # this will be the final endpoint after all processing...
- # ... and immediately, if the user isn't authorized
$c->detach unless $c->user_exists;
- $c->detach unless $c->user->has_permission_to(moderate => $problem->bodies_str_ids);
$c->forward('/auth/check_csrf_token');
- my $original = $problem->find_or_new_related( moderation_original_data => {
+ $c->stash->{history} = $problem->new_related( moderation_original_data => {
title => $problem->title,
detail => $problem->detail,
photo => $problem->photo,
anonymous => $problem->anonymous,
+ longitude => $problem->longitude,
+ latitude => $problem->latitude,
+ category => $problem->category,
+ $problem->extra ? (extra => $problem->extra) : (),
});
+ $c->stash->{original} = $problem->moderation_original_data || $c->stash->{history};
$c->stash->{problem} = $problem;
- $c->stash->{problem_original} = $original;
$c->stash->{moderation_reason} = $c->get_param('moderation_reason') // '';
}
sub moderate_report : Chained('report') : PathPart('') : Args(0) {
my ($self, $c) = @_;
+ my $problem = $c->stash->{problem};
+
+ # Make sure user can moderate this report
+ $c->detach unless $c->user->can_moderate($problem);
+
+ $c->forward('check_edited_elsewhere');
$c->forward('report_moderate_hide');
my @types = grep $_,
- $c->forward('report_moderate_title'),
- $c->forward('report_moderate_detail'),
- $c->forward('report_moderate_anon'),
- $c->forward('report_moderate_photo');
+ $c->forward('moderate_state'),
+ ($c->user->can_moderate_title($problem, 1)
+ ? $c->forward('moderate_text', [ 'title' ])
+ : ()),
+ $c->forward('moderate_text', [ 'detail' ]),
+ $c->forward('moderate_boolean', [ 'anonymous', 'show_name' ]),
+ $c->forward('moderate_boolean', [ 'photo' ]),
+ $c->forward('moderate_location'),
+ $c->forward('moderate_category'),
+ $c->forward('moderate_extra');
+
+ # Deal with possible photo changes. If a moderate form uses a standard
+ # photo upload field (with upload_fileid, label and file upload handlers),
+ # this will allow photos to be changed, not just switched on/off. You will
+ # probably want a hidden field with problem_photo=1 to skip that check.
+ my $photo_edit_form = defined $c->get_param('photo1');
+ if ($photo_edit_form) {
+ $c->forward('/photo/process_photo');
+ if ( my $photo_error = delete $c->stash->{photo_error} ) {
+ $c->stash->{moderate_errors} ||= [];
+ push @{ $c->stash->{moderate_errors} }, $photo_error;
+ } else {
+ my $fileid = $c->stash->{upload_fileid};
+ if ($fileid ne $problem->photo) {
+ $problem->get_photoset->delete_cached;
+ $problem->photo($fileid || undef);
+ push @types, 'photo';
+ }
+ }
+ }
- $c->detach( 'report_moderate_audit', \@types )
+ $c->detach( 'report_moderate_audit', \@types );
}
-sub moderating_user_name {
- my $user = shift;
- return $user->from_body ? $user->from_body->name : _('an administrator');
+sub check_edited_elsewhere : Private {
+ my ($self, $c) = @_;
+
+ my $problem = $c->stash->{problem};
+ my $last_moderation = $problem->latest_moderation;
+ return unless $last_moderation;
+
+ my $form_started = $c->get_param('form_started') || 0;
+ if ($form_started && $form_started < $last_moderation->created->epoch) {
+ $c->stash->{moderate_errors} ||= [];
+ push @{$c->stash->{moderate_errors}},
+ _('Someone has moderated this report since you started.') . ' ' .
+ sprintf(_('Please <a href="#%s">check their changes</a> and resolve any differences.'),
+ 'update_m' . $last_moderation->id);
+ $c->detach;
+ }
}
-sub report_moderate_audit : Private {
- my ($self, $c, @types) = @_;
+sub moderate_log_entry : Private {
+ my ($self, $c, $object_type, @types) = @_;
my $user = $c->user->obj;
my $reason = $c->stash->{'moderation_reason'};
- my $problem = $c->stash->{problem} or die;
+ my $object = $object_type eq 'update' ? $c->stash->{comment} : $c->stash->{problem};
my $types_csv = join ', ' => @types;
+ my $log_reason = "($types_csv)";
+ $log_reason = "$reason $log_reason" if $reason;
+
+ # We attach the log to the moderation entry if present, or the object if not (hiding)
$c->model('DB::AdminLog')->create({
action => 'moderation',
user => $user,
- admin_user => moderating_user_name($user),
- object_id => $problem->id,
- object_type => 'problem',
- reason => (sprintf '%s (%s)', $reason, $types_csv),
+ admin_user => $user->moderating_user_name,
+ object_id => $c->stash->{history}->id || $object->id,
+ object_type => $c->stash->{history}->id ? 'moderation' : $object_type,
+ reason => $log_reason,
});
+}
+
+sub report_moderate_audit : Private {
+ my ($self, $c, @types) = @_;
+
+ my $problem = $c->stash->{problem} or die;
+
+ return unless @types; # If nothing moderated, nothing to do
+ return if $c->stash->{moderate_errors}; # Don't update anything if errors
+
+ # Okay, now update the report
+ $problem->update;
+
+ return if @types == 1 && $types[0] eq 'state'; # If only state changed, no log entry needed
+
+ # We've done some non-state moderation, save the history
+ $c->stash->{history}->insert;
+
+ $c->forward('moderate_log_entry', [ 'problem', @types ]);
if ($problem->user->email_verified && $c->cobrand->send_moderation_notifications) {
my $token = $c->model("DB::Token")->create({
@@ -109,6 +193,7 @@ sub report_moderate_audit : Private {
data => { id => $problem->id }
});
+ my $types_csv = join ', ' => @types;
$c->send_email( 'problem-moderated.txt', {
to => [ [ $problem->user->email, $problem->name ] ],
types => $types_csv,
@@ -116,6 +201,7 @@ sub report_moderate_audit : Private {
problem => $problem,
report_uri => $c->stash->{report_uri},
report_complain_uri => $c->stash->{cobrand_base} . '/contact?m=' . $token->token,
+ moderated_data => $c->stash->{history},
});
}
}
@@ -135,97 +221,153 @@ sub report_moderate_hide : Private {
}
}
-sub report_moderate_title : Private {
- my ( $self, $c ) = @_;
+sub moderate_text : Private {
+ my ($self, $c, $thing) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+ my $param = $c->stash->{comment} ? 'update_' : 'problem_';
- my $old_title = $problem->title;
- my $original_title = $original->title;
+ my $thing_for_original_table = $thing;
+ # Update 'text' field is stored in original table's 'detail' field
+ $thing_for_original_table = 'detail' if $c->stash->{comment} && $thing eq 'text';
- my $title = $c->get_param('problem_revert_title') ?
- $original_title
- : $c->get_param('problem_title');
+ my $old = $object->$thing;
+ my $original_thing = $c->stash->{original}->$thing_for_original_table;
- if ($title ne $old_title) {
- $original->insert unless $original->in_storage;
- $problem->update({ title => $title });
- return 'title';
- }
+ my $new = $c->get_param($param . 'revert_' . $thing) ?
+ $original_thing
+ : $c->get_param($param . $thing);
- return;
+ if ($new ne $old) {
+ $object->$thing($new);
+ return $thing_for_original_table;
+ }
}
-sub report_moderate_detail : Private {
- my ( $self, $c ) = @_;
+sub moderate_boolean : Private {
+ my ( $self, $c, $thing, $reverse ) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
-
- my $old_detail = $problem->detail;
- my $original_detail = $original->detail;
- my $detail = $c->get_param('problem_revert_detail') ?
- $original_detail
- : $c->get_param('problem_detail');
-
- if ($detail ne $old_detail) {
- $original->insert unless $original->in_storage;
- $problem->update({ detail => $detail });
- return 'detail';
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+ my $param = $c->stash->{comment} ? 'update_' : 'problem_';
+ my $original = $c->stash->{original}->photo;
+
+ return if $thing eq 'photo' && !$original;
+
+ my $new;
+ if ($reverse) {
+ $new = $c->get_param($param . $reverse) ? 0 : 1;
+ } else {
+ $new = $c->get_param($param . $thing) ? 1 : 0;
+ }
+ my $old = $object->$thing ? 1 : 0;
+
+ if ($new != $old) {
+ if ($thing eq 'photo') {
+ $object->$thing($new ? $original : undef);
+ $object->get_photoset->delete_cached;
+ } else {
+ $object->$thing($new);
+ }
+ return $thing;
}
- return;
}
-sub report_moderate_anon : Private {
- my ( $self, $c ) = @_;
+sub moderate_extra : Private {
+ my ($self, $c) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+
+ my $changed;
+ my @extra = grep { /^extra\./ } keys %{$c->req->params};
+ foreach (@extra) {
+ my ($field_name) = /extra\.(.*)/;
+ my $old = $object->get_extra_metadata($field_name) || '';
+ my $new = $c->get_param($_);
+ if ($new ne $old) {
+ $object->set_extra_metadata($field_name, $new);
+ $changed = 1;
+ }
+ }
+ if ($changed) {
+ return 'extra';
+ }
+}
- my $show_user = $c->get_param('problem_show_name') ? 1 : 0;
- my $anonymous = $show_user ? 0 : 1;
- my $old_anonymous = $problem->anonymous ? 1 : 0;
+sub moderate_location : Private {
+ my ($self, $c) = @_;
- if ($anonymous != $old_anonymous) {
+ my $problem = $c->stash->{problem};
- $original->insert unless $original->in_storage;
- $problem->update({ anonymous => $anonymous });
- return 'anonymous';
+ my $moved = $c->forward('/admin/report_edit_location', [ $problem ]);
+ if (!$moved) {
+ # New lat/lon isn't valid, show an error
+ $c->stash->{moderate_errors} ||= [];
+ push @{ $c->stash->{moderate_errors} }, _('Invalid location. New location must be covered by the same council.');
+ } elsif ($moved == 2) {
+ return 'location';
}
- return;
}
-sub report_moderate_photo : Private {
- my ( $self, $c ) = @_;
+# No update left at present
+sub moderate_category : Private {
+ my ($self, $c) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
+ return unless $c->get_param('category');
- return unless $original->photo;
+ # The admin category editing needs to know all the categories etc
+ $c->forward('/admin/categories_for_point');
- my $show_photo = $c->get_param('problem_show_photo') ? 1 : 0;
- my $old_show_photo = $problem->photo ? 1 : 0;
+ my $problem = $c->stash->{problem};
- if ($show_photo != $old_show_photo) {
- $original->insert unless $original->in_storage;
- $problem->update({ photo => $show_photo ? $original->photo : undef });
- return 'photo';
+ my $changed = $c->forward( '/admin/report_edit_category', [ $problem, 1 ] );
+ # It might need to set_report_extras in future
+ if ($changed) {
+ return 'category';
+ }
+}
+
+# Note that if a cobrand allows state moderation, then the moderation reason
+# given will be added as an update and thus be publicly available (unlike with
+# normal moderation).
+sub moderate_state : Private {
+ my ($self, $c) = @_;
+
+ my $new_state = $c->get_param('state');
+ return unless $new_state;
+
+ my $problem = $c->stash->{problem};
+ if ($problem->state ne $new_state) {
+ $problem->state($new_state);
+ $problem->add_to_comments( {
+ text => $c->stash->{moderation_reason},
+ created => \'current_timestamp',
+ confirmed => \'current_timestamp',
+ user_id => $c->user->id,
+ name => $c->user->from_body ? $c->user->from_body->name : $c->user->name,
+ state => 'confirmed',
+ mark_fixed => 0,
+ anonymous => $c->user->from_body ? 0 : 1,
+ problem_state => $new_state,
+ } );
+ return 'state';
}
- return;
}
sub update : Chained('report') : PathPart('update') : CaptureArgs(1) {
my ($self, $c, $id) = @_;
my $comment = $c->stash->{problem}->comments->find($id);
- my $original = $comment->find_or_new_related( moderation_original_data => {
+ # Make sure user can moderate this update
+ $c->detach unless $comment && $c->user->can_moderate($comment);
+
+ $c->stash->{history} = $comment->new_related( moderation_original_data => {
detail => $comment->text,
photo => $comment->photo,
anonymous => $comment->anonymous,
+ $comment->extra ? (extra => $comment->extra) : (),
});
$c->stash->{comment} = $comment;
- $c->stash->{comment_original} = $original;
+ $c->stash->{original} = $comment->moderation_original_data || $c->stash->{history};
}
sub moderate_update : Chained('update') : PathPart('') : Args(0) {
@@ -234,31 +376,16 @@ sub moderate_update : Chained('update') : PathPart('') : Args(0) {
$c->forward('update_moderate_hide');
my @types = grep $_,
- $c->forward('update_moderate_detail'),
- $c->forward('update_moderate_anon'),
- $c->forward('update_moderate_photo');
-
- $c->detach( 'update_moderate_audit', \@types )
-}
-
-sub update_moderate_audit : Private {
- my ($self, $c, @types) = @_;
-
- my $user = $c->user->obj;
- my $reason = $c->stash->{'moderation_reason'};
- my $problem = $c->stash->{problem} or die;
- my $comment = $c->stash->{comment} or die;
-
- my $types_csv = join ', ' => @types;
-
- $c->model('DB::AdminLog')->create({
- action => 'moderation',
- user => $user,
- admin_user => moderating_user_name($user),
- object_id => $comment->id,
- object_type => 'update',
- reason => (sprintf '%s (%s)', $reason, $types_csv),
- });
+ $c->forward('moderate_text', [ 'text' ]),
+ $c->forward('moderate_boolean', [ 'anonymous', 'show_name' ]),
+ $c->forward('moderate_extra'),
+ $c->forward('moderate_boolean', [ 'photo' ]);
+
+ if (@types) {
+ $c->stash->{history}->insert;
+ $c->stash->{comment}->update;
+ $c->detach('moderate_log_entry', [ 'update', @types ]);
+ }
}
sub update_moderate_hide : Private {
@@ -269,77 +396,10 @@ sub update_moderate_hide : Private {
if ($c->get_param('update_hide')) {
$comment->hide;
- $c->detach( 'update_moderate_audit', ['hide'] ); # break chain here.
- }
- return;
-}
-
-sub update_moderate_detail : Private {
- my ( $self, $c ) = @_;
-
- my $problem = $c->stash->{problem} or die;
- my $comment = $c->stash->{comment} or die;
- my $original = $c->stash->{comment_original};
-
- my $old_detail = $comment->text;
- my $original_detail = $original->detail;
- my $detail = $c->get_param('update_revert_detail') ?
- $original_detail
- : $c->get_param('update_detail');
-
- if ($detail ne $old_detail) {
- $original->insert unless $original->in_storage;
- $comment->update({ text => $detail });
- return 'detail';
- }
- return;
-}
-
-sub update_moderate_anon : Private {
- my ( $self, $c ) = @_;
-
- my $problem = $c->stash->{problem} or die;
- my $comment = $c->stash->{comment} or die;
- my $original = $c->stash->{comment_original};
-
- my $show_user = $c->get_param('update_show_name') ? 1 : 0;
- my $anonymous = $show_user ? 0 : 1;
- my $old_anonymous = $comment->anonymous ? 1 : 0;
-
- if ($anonymous != $old_anonymous) {
- $original->insert unless $original->in_storage;
- $comment->update({ anonymous => $anonymous });
- return 'anonymous';
- }
- return;
-}
-
-sub update_moderate_photo : Private {
- my ( $self, $c ) = @_;
-
- my $problem = $c->stash->{problem} or die;
- my $comment = $c->stash->{comment} or die;
- my $original = $c->stash->{comment_original};
-
- return unless $original->photo;
-
- my $show_photo = $c->get_param('update_show_photo') ? 1 : 0;
- my $old_show_photo = $comment->photo ? 1 : 0;
-
- if ($show_photo != $old_show_photo) {
- $original->insert unless $original->in_storage;
- $comment->update({ photo => $show_photo ? $original->photo : undef });
- return 'photo';
+ $c->detach('moderate_log_entry', [ 'update', 'hide' ]); # break chain here.
}
}
-sub return_text : Private {
- my ($self, $c, $text) = @_;
-
- $c->res->content_type('text/plain; charset=utf-8');
- $c->res->body( $text // '' );
-}
-
__PACKAGE__->meta->make_immutable;
1;
diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm
index 883ccc0ce..ed890ad82 100644
--- a/perllib/FixMyStreet/App/Controller/My.pm
+++ b/perllib/FixMyStreet/App/Controller/My.pm
@@ -97,6 +97,8 @@ sub planned_reorder : Private {
sub get_problems : Private {
my ($self, $c) = @_;
+ $c->stash->{page} = 'my';
+
my $p_page = $c->get_param('p') || 1;
$c->forward( '/reports/stash_report_filter_status' );
@@ -159,13 +161,12 @@ sub setup_page_data : Private {
my @categories = $c->stash->{problems_rs}->search({
state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
}, {
- columns => [ 'category', 'extra' ],
+ columns => [ 'category' ],
distinct => 1,
order_by => [ 'category' ],
} )->all;
$c->stash->{filter_categories} = \@categories;
- $c->stash->{page} = 'my';
my $pins = $c->stash->{pins};
FixMyStreet::Map::display_map(
$c,
@@ -209,7 +210,7 @@ sub planned_change : Path('planned/change') {
$c->res->content_type('application/json; charset=utf-8');
$c->res->body(encode_json({ outcome => $add ? 'add' : 'remove' }));
} else {
- $c->res->redirect( $c->uri_for_action('report/display', $id) );
+ $c->res->redirect( $c->uri_for_action('report/display', [ $id ]) );
}
}
diff --git a/perllib/FixMyStreet/App/Controller/Open311.pm b/perllib/FixMyStreet/App/Controller/Open311.pm
index 83b9b8202..841330e92 100644
--- a/perllib/FixMyStreet/App/Controller/Open311.pm
+++ b/perllib/FixMyStreet/App/Controller/Open311.pm
@@ -7,6 +7,7 @@ use namespace::autoclean;
use JSON::MaybeXS;
use XML::Simple;
use DateTime::Format::W3CDTF;
+use FixMyStreet::MapIt;
BEGIN { extends 'Catalyst::Controller'; }
@@ -164,9 +165,7 @@ sub get_services : Private {
if ($lat || $lon) {
my $area_types = $c->cobrand->area_types;
- my $all_areas = mySociety::MaPit::call('point',
- "4326/$lon,$lat",
- type => $area_types);
+ my $all_areas = FixMyStreet::MapIt::call('point', "4326/$lon,$lat", type => $area_types);
$categories = $categories->search( {
'body_areas.area_id' => [ keys %$all_areas ],
}, { join => { 'body' => 'body_areas' } } );
@@ -310,7 +309,8 @@ sub get_requests : Private {
delete $states->{unconfirmed};
delete $states->{submitted};
my $criteria = {
- state => [ keys %$states ]
+ state => [ keys %$states ],
+ non_public => 0,
};
my %rules = (
@@ -415,6 +415,7 @@ sub get_request : Private {
my $criteria = {
state => [ keys %$states ],
id => $id,
+ non_public => 0,
};
$c->forward( 'output_requests', [ $criteria ] );
}
diff --git a/perllib/FixMyStreet/App/Controller/Photo.pm b/perllib/FixMyStreet/App/Controller/Photo.pm
index f41702dcf..7b536a292 100644
--- a/perllib/FixMyStreet/App/Controller/Photo.pm
+++ b/perllib/FixMyStreet/App/Controller/Photo.pm
@@ -5,8 +5,8 @@ use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller'; }
use JSON::MaybeXS;
-use File::Path;
-use File::Slurp;
+use Path::Tiny;
+use Try::Tiny;
use FixMyStreet::App::Model::PhotoSet;
=head1 NAME
@@ -46,6 +46,9 @@ sub index :LocalRegex('^(c/)?([1-9]\d*)(?:\.(\d+))?(?:\.(full|tn|fp))?\.(?:jpeg|
my ( $self, $c ) = @_;
my ( $is_update, $id, $photo_number, $size ) = @{ $c->req->captures };
+ $photo_number ||= 0;
+ $size ||= '';
+
my $item;
if ( $is_update ) {
($item) = $c->model('DB::Comment')->search( {
@@ -77,8 +80,10 @@ sub output : Private {
my ( $self, $c, $photo ) = @_;
# Save to file
- File::Path::make_path( FixMyStreet->path_to( 'web', 'photo', 'c' )->stringify );
- File::Slurp::write_file( FixMyStreet->path_to( 'web', $c->req->path )->stringify, \$photo->{data} );
+ path(FixMyStreet->path_to('web', 'photo', 'c'))->mkpath;
+ my $out = FixMyStreet->path_to('web', $c->req->path);
+ my $symlink_exists = $photo->{symlink} ? symlink($photo->{symlink}, $out) : undef;
+ path($out)->spew_raw($photo->{data}) unless $symlink_exists;
$c->res->content_type( $photo->{content_type} );
$c->res->body( $photo->{data} );
@@ -101,8 +106,13 @@ sub upload : Local {
c => $c,
data_items => \@items,
});
-
- my $fileid = $photoset->data;
+ my $fileid = try {
+ $photoset->data;
+ } catch {
+ $c->log->debug("Photo upload failed.");
+ $c->stash->{photo_error} = _("Photo upload failed.");
+ return undef;
+ };
my $out;
if ($c->stash->{photo_error} || !$fileid) {
$c->res->status(500);
diff --git a/perllib/FixMyStreet/App/Controller/Questionnaire.pm b/perllib/FixMyStreet/App/Controller/Questionnaire.pm
index 58848f546..d2b0bf3f4 100755
--- a/perllib/FixMyStreet/App/Controller/Questionnaire.pm
+++ b/perllib/FixMyStreet/App/Controller/Questionnaire.pm
@@ -33,7 +33,8 @@ sub check_questionnaire : Private {
my $problem = $questionnaire->problem;
- if ( $unanswered && $questionnaire->whenanswered ) {
+ my $cutoff = DateTime->now()->subtract( minutes => 2 );
+ if ( $unanswered && $questionnaire->whenanswered && $questionnaire->whenanswered < $cutoff) {
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);
@@ -64,15 +65,8 @@ sub submit : Path('submit') {
my ( $self, $c ) = @_;
if (my $token = $c->get_param('token')) {
- if ($token eq '_test_') {
- $c->stash->{been_fixed} = $c->get_param('been_fixed');
- $c->stash->{new_state} = $c->get_param('new_state');
- $c->stash->{template} = 'questionnaire/completed.html';
- return;
- }
$c->forward('submit_standard');
} elsif (my $p = $c->get_param('problem')) {
- $c->detach('creator_fixed') if $p eq '_test_';
$c->forward('submit_creator_fixed');
} else {
$c->detach( '/page_error_404_not_found' );
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm
index 799985f8e..7f798f4f4 100644
--- a/perllib/FixMyStreet/App/Controller/Report.pm
+++ b/perllib/FixMyStreet/App/Controller/Report.pm
@@ -20,8 +20,8 @@ Show a report
=head2 index
-Redirect to homepage unless C<id> parameter in query, in which case redirect to
-'/report/$id'.
+Redirect to homepage unless we have a homepage template,
+in which case show that.
=cut
@@ -35,13 +35,13 @@ sub index : Path('') : Args(0) {
}
}
-=head2 report_display
+=head2 id
-Display a report.
+Load in ID, for use by chained pages.
=cut
-sub display : Path('') : Args(1) {
+sub id :PathPart('report') :Chained :CaptureArgs(1) {
my ( $self, $c, $id ) = @_;
if (
@@ -49,15 +49,17 @@ sub display : Path('') : Args(1) {
|| $id =~ m{ ^(\d+) \D .* $ }x # trailing garbage
)
{
- return $c->res->redirect( $c->uri_for($1), 301 );
+ $c->res->redirect( $c->uri_for($1), 301 );
+ $c->detach;
}
- $c->forward( '_display', [ $id ] );
+ $c->forward( 'load_problem_or_display_error', [ $id ] );
}
=head2 ajax
-Return JSON formatted details of a report
+Return JSON formatted details of a report.
+URL used by mobile app so remains /report/ajax/N.
=cut
@@ -65,40 +67,62 @@ sub ajax : Path('ajax') : Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash->{ajax} = 1;
- $c->forward( '_display', [ $id ] );
+ $c->forward('load_problem_or_display_error', [ $id ]);
+ $c->forward('display');
}
-sub _display : Private {
- my ( $self, $c, $id ) = @_;
+=head2 display
+
+Display a report.
+
+=cut
+
+sub display :PathPart('') :Chained('id') :Args(0) {
+ my ( $self, $c ) = @_;
$c->forward('/auth/get_csrf_token');
- $c->forward( 'load_problem_or_display_error', [ $id ] );
$c->forward( 'load_updates' );
$c->forward( 'format_problem_for_display' );
my $permissions = $c->stash->{_permissions} ||= $c->forward( 'check_has_permission_to',
- [ qw/report_inspect report_edit_category report_edit_priority/ ] );
+ [ qw/report_inspect report_edit_category report_edit_priority report_mark_private/ ] );
if (any { $_ } values %$permissions) {
$c->stash->{template} = 'report/inspect.html';
$c->forward('inspect');
}
}
-sub support : Path('support') : Args(0) {
+sub moderate_report :PathPart('moderate') :Chained('id') :Args(0) {
my ( $self, $c ) = @_;
- my $id = $c->get_param('id');
+ if ($c->user_exists && $c->user->can_moderate($c->stash->{problem})) {
+ $c->stash->{show_moderation} = 'report';
+ $c->stash->{template} = 'report/display.html';
+ $c->detach('display');
+ }
+ $c->res->redirect($c->stash->{problem}->url);
+}
+
+sub moderate_update :PathPart('moderate') :Chained('id') :Args(1) {
+ my ( $self, $c, $update_id ) = @_;
- my $uri =
- $id
- ? $c->uri_for( '/report', $id )
- : $c->uri_for('/');
+ my $comment = $c->stash->{problem}->comments->find($update_id);
+ if ($c->user_exists && $comment && $c->user->can_moderate($comment)) {
+ $c->stash->{show_moderation} = $update_id;
+ $c->stash->{template} = 'report/display.html';
+ $c->detach('display');
+ }
+ $c->res->redirect($c->stash->{problem}->url);
+}
- if ( $id && $c->cobrand->can_support_problems && $c->user && $c->user->from_body ) {
- $c->forward( 'load_problem_or_display_error', [ $id ] );
+sub support :Chained('id') :Args(0) {
+ my ( $self, $c ) = @_;
+
+ if ( $c->cobrand->can_support_problems && $c->user && $c->user->from_body ) {
$c->stash->{problem}->update( { interest_count => \'interest_count +1' } );
}
- $c->res->redirect( $uri );
+
+ $c->res->redirect($c->stash->{problem}->url);
}
sub load_problem_or_display_error : Private {
@@ -130,8 +154,8 @@ sub load_problem_or_display_error : Private {
# 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}) ) {
+ [ qw/report_inspect report_edit_category report_edit_priority report_mark_private / ] );
+ if ( !$c->user || ($c->user->id != $problem->user->id && !($permissions->{report_inspect} || $permissions->{report_mark_private})) ) {
$c->detach(
'/page_error_403_access_denied',
[ sprintf(_('That report cannot be viewed on %s.'), $c->stash->{site_name}) ]
@@ -140,7 +164,7 @@ sub load_problem_or_display_error : Private {
}
$c->stash->{problem} = $problem;
- if ( $c->user_exists && $c->user->has_permission_to(moderate => $problem->bodies_str_ids) ) {
+ if ( $c->user_exists && $c->user->can_moderate($problem) ) {
$c->stash->{problem_original} = $problem->find_or_new_related(
moderation_original_data => {
title => $problem->title,
@@ -162,14 +186,30 @@ sub load_updates : Private {
{ order_by => [ 'confirmed', 'id' ] }
);
- my $questionnaires = $c->model('DB::Questionnaire')->search(
+ my $questionnaires_still_open = $c->model('DB::Questionnaire')->search(
+ {
+ problem_id => $c->stash->{problem}->id,
+ whenanswered => { '!=', undef },
+ -or => [ {
+ # Any steady state open/closed
+ old_state => [ -and =>
+ { -in => [ FixMyStreet::DB::Result::Problem::closed_states, FixMyStreet::DB::Result::Problem::open_states ] },
+ \'= new_state',
+ ],
+ }, {
+ # Any reopening
+ new_state => 'confirmed',
+ } ]
+ },
+ { order_by => 'whenanswered' }
+ );
+
+ my $questionnaires_fixed = $c->model('DB::Questionnaire')->search(
{
problem_id => $c->stash->{problem}->id,
whenanswered => { '!=', undef },
- old_state => [ -and =>
- { -in => [ FixMyStreet::DB::Result::Problem::closed_states, FixMyStreet::DB::Result::Problem::open_states ] },
- \'= new_state',
- ]
+ old_state => { -not_in => [ FixMyStreet::DB::Result::Problem::fixed_states ] },
+ new_state => { -in => [ FixMyStreet::DB::Result::Problem::fixed_states ] },
},
{ order_by => 'whenanswered' }
);
@@ -182,13 +222,36 @@ sub load_updates : Private {
$questionnaires_with_updates{$qid} = $update;
}
}
- while (my $q = $questionnaires->next) {
+ while (my $q = $questionnaires_still_open->next) {
if (my $update = $questionnaires_with_updates{$q->id}) {
$update->set_extra_metadata('open_from_questionnaire', 1);
next;
}
push @combined, [ $q->whenanswered, $q ];
}
+ while (my $q = $questionnaires_fixed->next) {
+ next if $questionnaires_with_updates{$q->id};
+ push @combined, [ $q->whenanswered, $q ];
+ }
+
+ # And include moderation changes...
+ my $problem = $c->stash->{problem};
+ my $public_history = $c->cobrand->call_hook(public_moderation_history => $problem);
+ my $user_can_moderate = $c->user_exists && $c->user->can_moderate($problem);
+ if ($public_history || $user_can_moderate) {
+ my @history = $problem->moderation_history;
+ my $last_history = $problem;
+ foreach my $history (@history) {
+ push @combined, [ $history->created, {
+ id => 'm' . $history->id,
+ type => 'moderation',
+ last => $last_history,
+ entry => $history,
+ } ];
+ $last_history = $history;
+ }
+ }
+
@combined = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @combined;
$c->stash->{updates} = \@combined;
@@ -206,6 +269,9 @@ sub format_problem_for_display : Private {
my $problem = $c->stash->{problem};
+ # upload_fileid is used by the update form on this page
+ $c->stash->{problem_upload_fileid} = $problem->get_photoset->data;
+
( $c->stash->{latitude}, $c->stash->{longitude} ) =
map { Utils::truncate_coordinate($_) }
( $problem->latitude, $problem->longitude );
@@ -251,7 +317,7 @@ sub generate_map_tags : Private {
latitude => $problem->latitude,
longitude => $problem->longitude,
pins => $problem->used_map
- ? [ $problem->pin_data($c, 'report', type => 'big') ]
+ ? [ $problem->pin_data($c, 'report', type => 'big', draggable => 1) ]
: [],
);
@@ -271,22 +337,18 @@ users too about this change, at which point we can delete:
=cut
-sub delete :Local :Args(1) {
- my ( $self, $c, $id ) = @_;
+sub delete :Chained('id') :Args(0) {
+ my ($self, $c) = @_;
$c->forward('/auth/check_csrf_token');
- $c->forward( 'load_problem_or_display_error', [ $id ] );
my $p = $c->stash->{problem};
- my $uri = $c->uri_for( '/report', $id );
-
- return $c->res->redirect($uri) unless $c->user_exists;
+ return $c->res->redirect($p->url) unless $c->user_exists;
my $body = $c->user->obj->from_body;
- return $c->res->redirect($uri) unless $body;
-
- return $c->res->redirect($uri) unless $p->bodies->{$body->id};
+ return $c->res->redirect($p->url) unless $body;
+ return $c->res->redirect($p->url) unless $p->bodies->{$body->id};
$p->state('hidden');
$p->lastupdate( \'current_timestamp' );
@@ -299,26 +361,10 @@ sub delete :Local :Args(1) {
admin_user => $c->user->from_body->name,
object_type => 'problem',
action => 'state_change',
- object_id => $id,
+ object_id => $p->id,
} );
- return $c->res->redirect($uri);
-}
-
-=head2 action_router
-
-A router for dispatching handlers for sub-actions on a particular report,
-e.g. /report/1/inspect
-
-=cut
-
-sub action_router : Path('') : Args(2) {
- my ( $self, $c, $id, $action ) = @_;
-
- $c->go( 'map', [ $id ] ) if $action eq 'map';
- $c->go( 'nearby_json', [ $id ] ) if $action eq 'nearby.json';
-
- $c->detach( '/page_error_404_not_found', [] );
+ return $c->res->redirect($p->url);
}
sub inspect : Private {
@@ -327,7 +373,7 @@ sub inspect : Private {
my $permissions = $c->stash->{_permissions};
$c->forward('/admin/categories_for_point');
- $c->stash->{report_meta} = { map { $_->{name} => $_ } @{ $c->stash->{problem}->get_extra_fields() } };
+ $c->stash->{report_meta} = { map { 'x' . $_->{name} => $_ } @{ $c->stash->{problem}->get_extra_fields() } };
if ($c->cobrand->can('council_area_id')) {
my $priorities_by_category = FixMyStreet::App->model('DB::ResponsePriority')->by_categories($c->cobrand->council_area_id, @{$c->stash->{contacts}});
@@ -357,8 +403,6 @@ 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') ) {
@@ -375,12 +419,6 @@ sub inspect : Private {
}
}
- 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) {
@@ -438,6 +476,8 @@ sub inspect : Private {
}
}
+ $problem->non_public($c->get_param('non_public') ? 1 : 0);
+
if ( !$c->forward( '/admin/report_edit_location', [ $problem ] ) ) {
# New lat/lon isn't valid, show an error
$valid = 0;
@@ -461,11 +501,26 @@ sub inspect : Private {
$c->forward('/report/new/set_report_extras', [ \@contacts, $param_prefix ]);
}
- # Updating priority must come after category, in case category has changed (and so might have priorities)
- if ($c->get_param('priority') && ($permissions->{report_inspect} || $permissions->{report_edit_priority})) {
- $problem->response_priority( $problem->response_priorities->find({ id => $c->get_param('priority') }) );
+ # Updating priority/defect type must come after category, in case
+ # category has changed (and so might have priorities/defect types)
+ if ($permissions->{report_inspect} || $permissions->{report_edit_priority}) {
+ if ($c->get_param('priority')) {
+ $problem->response_priority( $problem->response_priorities->find({ id => $c->get_param('priority') }) );
+ } else {
+ $problem->response_priority(undef);
+ }
}
+ if ($permissions->{report_inspect}) {
+ if ( $c->get_param('defect_type') ) {
+ $problem->defect_type($problem->defect_types->find($c->get_param('defect_type')));
+ } else {
+ $problem->defect_type(undef);
+ }
+ }
+
+ $c->cobrand->call_hook(report_inspect_update_extra => $problem);
+
if ($valid) {
if ( $reputation_change != 0 ) {
$problem->user->update_reputation($reputation_change);
@@ -479,7 +534,7 @@ sub inspect : Private {
# 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,
+ time_zone => FixMyStreet->local_time_zone,
epoch => $saved_at
);
}
@@ -508,7 +563,7 @@ sub inspect : Private {
# shortlist is always a single click away, being on the main nav.
if ($c->user->has_body_permission_to('planned_reports')) {
unless ($redirect_uri = $c->get_param("post_inspect_url")) {
- my $categories = join(',', @{ $c->user->categories });
+ my $categories = $c->user->categories_string;
my $params = {
lat => $problem->latitude,
lon => $problem->longitude,
@@ -532,10 +587,8 @@ sub inspect : Private {
}
};
-sub map : Private {
- my ( $self, $c, $id ) = @_;
-
- $c->forward( 'load_problem_or_display_error', [ $id ] );
+sub map :Chained('id') :Args(0) {
+ my ($self, $c) = @_;
my $image = $c->stash->{problem}->static_map;
$c->res->content_type($image->{content_type});
@@ -543,27 +596,44 @@ sub map : Private {
}
-sub nearby_json : Private {
- my ( $self, $c, $id ) = @_;
+sub nearby_json :PathPart('nearby.json') :Chained('id') :Args(0) {
+ my ($self, $c) = @_;
- $c->forward( 'load_problem_or_display_error', [ $id ] );
my $p = $c->stash->{problem};
- my $dist = 1;
+ $c->forward('_nearby_json', [ {
+ latitude => $p->latitude,
+ longitude => $p->longitude,
+ categories => [ $p->category ],
+ ids => [ $p->id ],
+ } ]);
+}
+
+sub _nearby_json :Private {
+ my ($self, $c, $params) = @_;
# 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');
+ # distance in metres
+ my $dist = $c->get_param('distance') || '';
+ $dist = 1000 unless $dist =~ /^\d+$/;
+ $dist = 1000 if $dist > 1000;
+ $params->{distance} = $dist / 1000;
+
+ my $pin_size = $c->get_param('pin_size') || '';
+ $pin_size = 'small' unless $pin_size =~ /^(mini|small|normal|big)$/;
+
+ $params->{extra} = $c->cobrand->call_hook('display_location_extra_params');
+ $params->{limit} = 5;
+
+ my $nearby = $c->model('DB::Nearby')->nearby($c, %$params);
- my $nearby = $c->model('DB::Nearby')->nearby(
- $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 ];
my @pins = map {
my $p = $_->pin_data($c, 'around');
[ $p->{latitude}, $p->{longitude}, $p->{colour},
- $p->{id}, $p->{title}, 'small', JSON->false
+ $p->{id}, $p->{title}, $pin_size, JSON->false
]
} @$nearby;
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index b5e5c5738..8944a9307 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -6,13 +6,14 @@ BEGIN { extends 'Catalyst::Controller'; }
use Encode;
use List::MoreUtils qw(uniq);
+use List::Util 'first';
use POSIX 'strcoll';
use HTML::Entities;
-use mySociety::MaPit;
use Path::Class;
use Utils;
use mySociety::EmailUtil;
use JSON::MaybeXS;
+use Text::CSV;
use FixMyStreet::SMS;
=head1 NAME
@@ -95,7 +96,7 @@ sub report_new : Path : Args(0) {
# 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')
- if !$c->forward('determine_location') || $c->get_param('filter_update');
+ if !$c->forward('determine_location') || $c->get_param('pc_override') || $c->get_param('filter_update');
# create a problem from the submitted details
$c->stash->{template} = "report/new/fill_in_details.html";
@@ -117,12 +118,6 @@ sub report_new : Path : Args(0) {
$c->forward('redirect_or_confirm_creation');
}
-sub report_new_test : Path('_test_') : Args(0) {
- my ( $self, $c ) = @_;
- $c->stash->{template} = 'email_sent.html';
- $c->stash->{email_type} = $c->get_param('email_type');
-}
-
# This is for the new phonegap versions of the app. It looks a lot like
# report_new but there's a few workflow differences as we only ever want
# to sent JSON back here
@@ -187,10 +182,8 @@ sub report_form_ajax : Path('ajax') : Args(0) {
# work out the location for this report and do some checks
if ( ! $c->forward('determine_location') ) {
- my $body = encode_json({ error => $c->stash->{location_error} });
- $c->res->content_type('application/json; charset=utf-8');
- $c->res->body($body);
- return;
+ $c->stash->{json_response} = { error => $c->stash->{location_error} };
+ $c->detach('send_json_response');
}
$c->forward('setup_categories_and_bodies');
@@ -207,6 +200,7 @@ sub report_form_ajax : Path('ajax') : Args(0) {
my $extra_titles_list = $c->cobrand->title_list($c->stash->{all_areas});
+ my @list_of_names = map { $_->name } values %{$c->stash->{bodies}};
my $contribute_as = {};
if ($c->user_exists) {
my @bodies = keys %{$c->stash->{bodies}};
@@ -218,20 +212,27 @@ sub report_form_ajax : Path('ajax') : Args(0) {
$contribute_as->{body} = $ca_body if $ca_body;
}
- my $body = encode_json(
- {
- councils_text => $councils_text,
- councils_text_private => $councils_text_private,
- category => $category,
- extra_name_info => $extra_name_info,
- titles_list => $extra_titles_list,
- %$contribute_as ? (contribute_as => $contribute_as) : (),
- $top_message ? (top_message => $top_message) : (),
- }
- );
+ my %by_category;
+ foreach my $contact (@{$c->stash->{category_options}}) {
+ next if ref $contact eq 'HASH'; # Ignore the 'Pick a category' line
+ my $cat = $c->stash->{category} = $contact->category;
+ my $body = $c->forward('by_category_ajax_data', [ 'all', $cat ]);
+ $by_category{$cat} = $body;
+ }
- $c->res->content_type('application/json; charset=utf-8');
- $c->res->body($body);
+ $c->stash->{json_response} = {
+ bodies => \@list_of_names,
+ councils_text => $councils_text,
+ councils_text_private => $councils_text_private,
+ category => $category,
+ extra_name_info => $extra_name_info,
+ titles_list => $extra_titles_list,
+ %$contribute_as ? (contribute_as => $contribute_as) : (),
+ $top_message ? (top_message => $top_message) : (),
+ unresponsive => $c->stash->{unresponsive}->{ALL} || '',
+ by_category => \%by_category,
+ };
+ $c->detach('send_json_response');
}
sub category_extras_ajax : Path('category_extras') : Args(0) {
@@ -239,53 +240,60 @@ sub category_extras_ajax : Path('category_extras') : Args(0) {
$c->forward('initialize_report');
if ( ! $c->forward('determine_location') ) {
- my $body = encode_json({ error => _("Sorry, we could not find that location.") });
- $c->res->content_type('application/json; charset=utf-8');
- $c->res->body($body);
- return 1;
+ $c->stash->{json_response} = { error => _("Sorry, we could not find that location.") };
+ $c->detach('send_json_response');
}
$c->forward('setup_categories_and_bodies');
$c->forward('setup_report_extra_fields');
- $c->forward('check_for_category');
+ $c->forward('check_for_category');
my $category = $c->stash->{category} || "";
$category = '' if $category eq _('-- Pick a category --');
- my $bodies = $c->forward('contacts_to_bodies', [ $category ]);
- my $vars = {
- $category ? (list_of_names => [ map { $_->name } @$bodies ]) : (),
- };
+ $c->stash->{json_response} = $c->forward('by_category_ajax_data', [ 'one', $category ]);
+ $c->forward('send_json_response');
+}
+
+sub by_category_ajax_data : Private {
+ my ($self, $c, $type, $category) = @_;
- my $category_extra = '';
- my $category_extra_json = [];
my $generate;
- if ( $c->stash->{category_extras}->{$category} && @{ $c->stash->{category_extras}->{$category} } >= 1 ) {
- $c->stash->{category_extras} = { $category => $c->stash->{category_extras}->{$category} };
- $generate = 1;
- }
- if ($c->stash->{unresponsive}->{$category}) {
- $generate = 1;
- }
- if ($c->stash->{report_extra_fields}) {
+ if (($c->stash->{category_extras}->{$category} && @{ $c->stash->{category_extras}->{$category} } >= 1) or
+ $c->stash->{unresponsive}->{$category} or $c->stash->{report_extra_fields}) {
$generate = 1;
}
+
+ my $bodies = $c->forward('contacts_to_bodies', [ $category ]);
+ my $list_of_names = [ map { $_->name } ($category ? @$bodies : values %{$c->stash->{bodies_to_list}}) ];
+ my $vars = {
+ $category ? (list_of_names => $list_of_names) : (),
+ };
+
+ my $body = {
+ bodies => $list_of_names,
+ };
+
if ($generate) {
- $category_extra = $c->render_fragment('report/new/category_extras.html', $vars);
- $category_extra_json = $c->forward('generate_category_extra_json');
+ $body->{category_extra} = $c->render_fragment('report/new/category_extras.html', $vars);
+ $body->{category_extra_json} = $c->forward('generate_category_extra_json');
+
}
- my $councils_text = $c->render_fragment( 'report/new/councils_text.html', $vars);
- my $councils_text_private = $c->render_fragment( 'report/new/councils_text_private.html');
+ my $unresponsive = $c->stash->{unresponsive}->{$category};
+ $unresponsive ||= $c->stash->{unresponsive}->{ALL} || '' if $type eq 'one';
- my $body = encode_json({
- category_extra => $category_extra,
- councils_text => $councils_text,
- councils_text_private => $councils_text_private,
- category_extra_json => $category_extra_json,
- });
+ # unresponsive must return empty string if okay, as that's what mobile app checks
+ if ($type eq 'one' || ($type eq 'all' && $unresponsive)) {
+ $body->{unresponsive} = $unresponsive;
+ # Check for no bodies here, because if there are any (say one
+ # unresponsive, one not), can use default display code for that.
+ if ($type eq 'all' && !@$bodies) {
+ $body->{councils_text} = $c->render_fragment( 'report/new/councils_text.html', $vars);
+ $body->{councils_text_private} = $c->render_fragment( 'report/new/councils_text_private.html');
+ }
+ }
- $c->res->content_type('application/json; charset=utf-8');
- $c->res->body($body);
+ return $body;
}
=head2 report_import
@@ -412,6 +420,12 @@ sub report_import : Path('/import') {
$c->send_email( 'partial.txt', { to => $report->user->email, } );
+ if ( $c->get_param('web') ) {
+ $c->res->content_type('text/html; charset=utf-8');
+ $c->stash->{template} = 'email_sent.html';
+ $c->stash->{email_type} = 'problem';
+ return 1;
+ }
$c->res->body('SUCCESS');
return 1;
}
@@ -469,6 +483,9 @@ sub initialize_report : Private {
# save the token to delete at the end
$c->stash->{partial_token} = $token if $report;
+ $c->stash->{email} = $report->user->email;
+ $c->stash->{phone} = $report->user->phone_display;
+
} else {
# no point keeping it if it is done.
$token->delete;
@@ -619,13 +636,12 @@ sub setup_categories_and_bodies : Private {
my @bodies = $c->model('DB::Body')->active->for_areas(keys %$all_areas)->all;
my %bodies = map { $_->id => $_ } @bodies;
- my $first_body = ( values %bodies )[0];
my $contacts #
= $c #
->model('DB::Contact') #
->active
- ->search( { body_id => [ keys %bodies ] }, { prefetch => 'body' } );
+ ->search( { 'me.body_id' => [ keys %bodies ] }, { prefetch => 'body' } );
my @contacts = $c->cobrand->categories_restriction($contacts)->all;
# variables to populate
@@ -638,17 +654,18 @@ sub setup_categories_and_bodies : Private {
(); # categories for which the reports are not public
$c->stash->{unresponsive} = {};
- if (keys %bodies == 1 && $first_body->send_method && $first_body->send_method eq 'Refused') {
- # If there's only one body, and it's set to refused, we can show the
+ my @refused_bodies = grep { ($_->send_method || "") eq 'Refused' } values %bodies;
+ if (@refused_bodies && @refused_bodies == values %bodies) {
+ # If all bodies are set to Refused, we can show the
# message immediately, before they select a category.
+ my $k = 'ALL';
if ($c->action->name eq 'category_extras_ajax' && $c->req->method eq 'POST') {
# The mobile app doesn't currently use this, in which case make
# sure the message is output, either below with a category, or when
# a blank category call is made.
- $c->stash->{unresponsive}{""} = $first_body->id;
- } else {
- $c->stash->{unresponsive}{ALL} = $first_body->id;
+ $k = "";
}
+ $c->stash->{unresponsive}{$k} = { map { $_ => 1 } keys %bodies };
}
# keysort does not appear to obey locale so use strcoll (see i18n.t)
@@ -665,20 +682,25 @@ sub setup_categories_and_bodies : Private {
$bodies_to_list{ $contact->body_id } = $contact->body;
- unless ( $seen{$contact->category} ) {
- push @category_options, $contact;
+ my $metas = $contact->get_metadata_for_input;
+ if (@$metas) {
+ push @{$category_extras{$contact->category}}, @$metas;
+ my $all_hidden = (grep { !$c->cobrand->category_extra_hidden($_) } @$metas) ? 0 : 1;
+ if (exists($category_extras_hidden{$contact->category})) {
+ $category_extras_hidden{$contact->category} &&= $all_hidden;
+ } else {
+ $category_extras_hidden{$contact->category} = $all_hidden;
+ }
+ }
- 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;
+ $non_public_categories{ $contact->category } = 1 if $contact->non_public;
- my $body_send_method = $bodies{$contact->body_id}->send_method || '';
- $c->stash->{unresponsive}{$contact->category} = $contact->body_id
- if !$c->stash->{unresponsive}{ALL} &&
- ($contact->email =~ /^REFUSED$/i || $body_send_method eq 'Refused');
+ my $body_send_method = $contact->body->send_method || '';
+ $c->stash->{unresponsive}{$contact->category}{$contact->body_id} = 1
+ if !$c->stash->{unresponsive}{ALL} &&
+ ($contact->email =~ /^REFUSED$/i || $body_send_method eq 'Refused');
- $non_public_categories{ $contact->category } = 1 if $contact->non_public;
- }
+ push @category_options, $contact unless $seen{$contact->category};
$seen{$contact->category} = $contact;
}
@@ -702,6 +724,12 @@ sub setup_categories_and_bodies : Private {
$c->stash->{non_public_categories} = \%non_public_categories;
$c->stash->{extra_name_info} = $first_area->{id} == COUNCIL_ID_BROMLEY ? 1 : 0;
+ # escape these so we can then split on , cleanly in the template.
+ my @list_of_names = map { $_->name } values %bodies_to_list;
+ my $csv = Text::CSV->new();
+ $csv->combine(@list_of_names);
+ $c->stash->{list_of_names_as_string} = $csv->string;
+
my @missing_details_bodies = grep { !$bodies_to_list{$_->id} } values %bodies;
my @missing_details_body_names = map { $_->name } @missing_details_bodies;
@@ -729,7 +757,7 @@ sub setup_report_extra_fields : Private {
return unless $c->cobrand->allow_report_extra_fields;
- my @extras = $c->model('DB::ReportExtraFields')->for_cobrand($c->cobrand)->for_language($c->stash->{lang_code})->all;
+ my @extras = $c->model('DB::ReportExtraField')->for_cobrand($c->cobrand)->for_language($c->stash->{lang_code})->all;
$c->stash->{report_extra_fields} = \@extras;
}
@@ -761,7 +789,10 @@ sub process_user : Private {
# Extract all the params to a hash to make them easier to work with
my %params = map { $_ => $c->get_param($_) }
- ( 'username', 'email', 'name', 'phone', 'password_register', 'fms_extra_title' );
+ ( 'email', 'name', 'phone', 'password_register', 'fms_extra_title' );
+
+ # Report form includes two username fields: #form_username_register and #form_username_sign_in
+ $params{username} = (first { $_ } $c->get_param_list('username')) || '';
if ( $c->cobrand->allow_anonymous_reports ) {
my $anon_details = $c->cobrand->anonymous_account;
@@ -782,9 +813,13 @@ sub process_user : Private {
$report->user( $user );
$c->forward('update_user', [ \%params ]);
+ $c->stash->{phone} = $report->user->phone_display;
+ $c->stash->{email} = $report->user->email;
+
if ($c->stash->{contributing_as_body} or $c->stash->{contributing_as_anonymous_user}) {
- $report->name($user->from_body->name);
- $user->name($user->from_body->name) unless $user->name;
+ my $name = $user->moderating_user_name;
+ $report->name($name);
+ $user->name($name) unless $user->name;
$c->stash->{no_reporter_alert} = 1;
}
@@ -805,6 +840,12 @@ sub process_user : Private {
$c->stash->{phone_may_be_mobile} = $type eq 'phone' && $parsed->{may_be_mobile};
+ $c->forward('update_user', [ \%params ]);
+
+ $c->stash->{phone} = Utils::trim_text( $type eq 'phone' ? $report->user->phone_display : $params{phone} );
+ $c->stash->{email} = Utils::trim_text( $type eq 'email' ? $report->user->email : $params{email} );
+
+
# The user is trying to sign in. We only care about username from the params.
if ( $c->get_param('submit_sign_in') || $c->get_param('password_sign_in') ) {
$c->stash->{tfa_data} = {
@@ -825,7 +866,6 @@ sub process_user : Private {
return 1;
}
- $c->forward('update_user', [ \%params ]);
if ($params{password_register}) {
$c->forward('/auth/test_password', [ $params{password_register} ]);
$report->user->password($params{password_register});
@@ -872,7 +912,6 @@ sub process_report : Private {
'partial', #
'service', #
'non_public',
- 'single_body_only'
);
# load the report
@@ -931,8 +970,20 @@ sub process_report : Private {
return 1;
}
- my $bodies = $c->forward('contacts_to_bodies', [ $report->category, $params{single_body_only} ]);
- my $body_string = join(',', map { $_->id } @$bodies) || '-1';
+ # 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.
+ my $body_string = do {
+ if (my $single_body_only = $c->get_param('single_body_only')) {
+ my $body = $c->model('DB::Body')->search({ name => $single_body_only })->first;
+ $body ? $body->id : '-1';
+ } else {
+ my $contact_options = {};
+ $contact_options->{do_not_send} = [ $c->get_param_list('do_not_send', 1) ];
+ my $bodies = $c->forward('contacts_to_bodies', [ $report->category, $contact_options ]);
+ join(',', map { $_->id } @$bodies) || '-1';
+ }
+ };
$report->bodies_str($body_string);
# Record any body IDs which might have meant to match, but had no contact
@@ -988,59 +1039,46 @@ sub process_report : Private {
}
sub contacts_to_bodies : Private {
- my ($self, $c, $category, $single_body_only) = @_;
+ my ($self, $c, $category, $options) = @_;
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;
+ # check that the front end has not indicated that we should not send to a
+ # body. This is usually because the asset code thinks it's not near enough
+ # to a road.
+ if ($options->{do_not_send}) {
+ my %do_not_send_check = map { $_ => 1 } @{$options->{do_not_send}};
+ my @contacts_filtered = grep { !$do_not_send_check{$_->body->name} } @contacts;
@contacts = @contacts_filtered if scalar @contacts_filtered;
}
- if ($c->stash->{unresponsive}{$category} || $c->stash->{unresponsive}{ALL} || !@contacts) {
- [];
- } else {
+ my $unresponsive = $c->stash->{unresponsive}{$category} || $c->stash->{unresponsive}{ALL};
+ if ($unresponsive) {
+ @contacts = grep { !$unresponsive->{$_->body_id} } @contacts;
+ } elsif (@contacts) {
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 ];
- } else {
- [ map { $_->body } @contacts ];
+ @contacts = ($contacts[0]);
}
}
+ [ map { $_->body } @contacts ];
}
sub set_report_extras : Private {
my ($self, $c, $contacts, $param_prefix) = @_;
$param_prefix ||= "";
- my @extra;
- 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)) {
- unless ( $c->get_param($param_prefix . $field->{code}) ) {
- $c->stash->{field_errors}->{ $field->{code} } = _('This information is required');
- }
- }
- push @extra, {
- name => $field->{code},
- description => $field->{description},
- value => $c->get_param($param_prefix . $field->{code}) || '',
- };
- }
- }
+ my @metalist = map { [ $_->get_metadata_for_input, $param_prefix ] } @$contacts;
+ push @metalist, map { [ $_->get_extra_fields, "extra[" . $_->id . "]" ] } @{$c->stash->{report_extra_fields}};
- foreach my $extra_fields (@{ $c->stash->{report_extra_fields} }) {
- my $metas = $extra_fields->get_extra_fields;
- $param_prefix = "extra[" . $extra_fields->id . "]";
+ my @extra;
+ foreach my $item (@metalist) {
+ my ($metas, $param_prefix) = @$item;
foreach my $field ( @$metas ) {
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');
+ $c->stash->{field_errors}->{ 'x' . $field->{code} } = _('This information is required');
}
}
push @extra, {
@@ -1055,7 +1093,7 @@ sub set_report_extras : Private {
if ( scalar @$contacts );
if ( @extra ) {
- $c->stash->{report_meta} = { map { $_->{name} => $_ } @extra };
+ $c->stash->{report_meta} = { map { 'x' . $_->{name} => $_ } @extra };
$c->stash->{report}->set_extra_fields( @extra );
}
}
@@ -1232,10 +1270,9 @@ sub process_confirmation : Private {
}
# We have an unconfirmed problem
+ $problem->confirm;
$problem->update(
{
- state => 'confirmed',
- confirmed => \'current_timestamp',
lastupdate => \'current_timestamp',
}
);
@@ -1401,7 +1438,8 @@ sub generate_map : Private {
pins => [ {
latitude => $latitude,
longitude => $longitude,
- colour => 'green', # 'yellow',
+ draggable => 1,
+ colour => $c->cobrand->pin_new_report_colour,
} ],
);
}
@@ -1504,8 +1542,17 @@ sub redirect_to_around : Private {
foreach (qw(pc zoom)) {
$params->{$_} = $c->get_param($_);
}
+
+ if (my $pc_override = $c->get_param('pc_override')) {
+ delete $params->{lat};
+ delete $params->{lon};
+ $params->{pc} = $pc_override;
+ }
+
+ my $csv = Text::CSV->new;
foreach (qw(status filter_category)) {
- $params->{$_} = join(',', $c->get_param_list($_, 1));
+ $csv->combine($c->get_param_list($_, 1));
+ $params->{$_} = $csv->string;
}
# delete empty values
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index 4a5b8db5d..cbedf7a01 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -5,6 +5,7 @@ use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
use Path::Class;
+use List::Util 'first';
use Utils;
=head1 NAME
@@ -23,14 +24,14 @@ sub report_update : Path : Args(0) {
$c->forward('initialize_update');
$c->forward('load_problem');
$c->forward('check_form_submitted')
- or $c->go( '/report/display', [ $c->stash->{problem}->id ] );
+ or $c->go( '/report/display', [ $c->stash->{problem}->id ], [] );
$c->forward('/auth/check_csrf_token');
$c->forward('process_update');
$c->forward('process_user');
$c->forward('/photo/process_photo');
$c->forward('check_for_errors')
- or $c->go( '/report/display', [ $c->stash->{problem}->id ] );
+ or $c->go( '/report/display', [ $c->stash->{problem}->id ], [] );
$c->forward('save_update');
$c->forward('redirect_or_confirm_creation');
@@ -99,7 +100,10 @@ sub process_user : Private {
# Extract all the params to a hash to make them easier to work with
my %params = map { $_ => $c->get_param($_) }
- ( 'username', 'name', 'password_register', 'fms_extra_title' );
+ ( 'name', 'password_register', 'fms_extra_title' );
+
+ # Update form includes two username fields: #form_username_register and #form_username_sign_in
+ $params{username} = (first { $_ } $c->get_param_list('username')) || '';
# Extra block to use 'last'
if ( $c->user_exists ) { {
@@ -241,6 +245,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 if $c->cobrand->call_hook(updates_disallowed => $c->stash->{problem});
return $c->get_param('submit_update') || '';
}
@@ -342,7 +347,7 @@ sub check_for_errors : Private {
my $state = $c->get_param('state');
if ( $state && $state ne $c->stash->{update}->problem->state ) {
my $error = 0;
- $error = 1 unless $c->user && $c->user->belongs_to_body( $c->stash->{update}->problem->bodies_str );
+ $error = 1 unless $c->user && ($c->user->is_superuser || $c->user->belongs_to_body($c->stash->{update}->problem->bodies_str));
$error = 1 unless grep { $state eq $_ } FixMyStreet::DB::Result::Problem->visible_states();
if ( $error ) {
$c->stash->{errors} ||= [];
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
index dc9e2c913..49bdce379 100644
--- a/perllib/FixMyStreet/App/Controller/Reports.pm
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -6,7 +6,7 @@ use JSON::MaybeXS;
use List::MoreUtils qw(any);
use Path::Tiny;
use RABX;
-use mySociety::MaPit;
+use FixMyStreet::MapIt;
BEGIN { extends 'Catalyst::Controller'; }
@@ -31,26 +31,7 @@ Show the summary page of all reports.
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
- # Zurich goes straight to map page, with all reports
- if ( $c->cobrand->moniker eq 'zurich' ) {
- $c->forward( 'stash_report_filter_status' );
- $c->forward( 'load_and_group_problems' );
- $c->stash->{body} = { id => 0 }; # So template can fetch the list
-
- if ($c->get_param('ajax')) {
- $c->detach('ajax', [ 'reports/_problem-list.html' ]);
- }
-
- my $pins = $c->stash->{pins};
- $c->stash->{page} = 'reports';
- FixMyStreet::Map::display_map(
- $c,
- latitude => @$pins ? $pins->[0]{latitude} : 0,
- longitude => @$pins ? $pins->[0]{longitude} : 0,
- area => 274456,
- pins => $pins,
- any_zoom => 1,
- );
+ if ( $c->cobrand->call_hook('report_page_data') ) {
return 1;
}
@@ -59,14 +40,7 @@ sub index : Path : Args(0) {
$c->detach( 'redirect_body' );
}
- if (my $body = $c->get_param('body')) {
- $body = $c->model('DB::Body')->find( { id => $body } );
- if ($body) {
- $body = $c->cobrand->short_name($body);
- $c->res->redirect("/reports/$body");
- $c->detach;
- }
- }
+ $c->forward('display_body_stats');
my $dashboard = $c->forward('load_dashboard_data');
@@ -92,13 +66,34 @@ sub index : Path : Args(0) {
$c->stash->{children} = $children;
}
} else {
- my @bodies = $c->model('DB::Body')->active->translated->with_area_count->all_sorted;
+ my @bodies = $c->model('DB::Body')->search(undef, {
+ columns => [ "id", "name" ],
+ })->active->translated->with_area_count->all_sorted;
@bodies = @{$c->cobrand->call_hook('reports_hook_restrict_bodies_list', \@bodies) || \@bodies };
$c->stash->{bodies} = \@bodies;
}
# Down here so that error pages aren't cached.
- $c->response->header('Cache-Control' => 'max-age=3600');
+ my $max_age = FixMyStreet->config('CACHE_TIMEOUT') // 3600;
+ $c->response->header('Cache-Control' => 'max-age=' . $max_age);
+}
+
+=head2 display_body_stats
+
+Show the stats for a body if body param is set.
+
+=cut
+
+sub display_body_stats : Private {
+ my ( $self, $c ) = @_;
+ if (my $body = $c->get_param('body')) {
+ $body = $c->model('DB::Body')->find( { id => $body } );
+ if ($body) {
+ $body = $c->cobrand->short_name($body);
+ $c->res->redirect("/reports/$body");
+ $c->detach;
+ }
+ }
}
=head2 body
@@ -123,7 +118,7 @@ sub ward : Path : Args(2) {
$c->forward('/auth/get_csrf_token');
- my @wards = split /\|/, $ward || "";
+ my @wards = $c->get_param('wards') ? $c->get_param_list('wards', 1) : split /\|/, $ward || "";
$c->forward( 'body_check', [ $body ] );
# If viewing multiple wards, rewrite the url from
@@ -150,6 +145,8 @@ sub ward : Path : Args(2) {
$c->go('index');
}
+ $c->stash->{page} = 'reports'; # So the map knows to make clickable pins
+
$c->forward( 'ward_check', [ @wards ] )
if @wards;
$c->forward( 'check_canonical_url', [ $body ] );
@@ -157,7 +154,8 @@ sub ward : Path : Args(2) {
$c->forward( 'load_and_group_problems' );
if ($c->get_param('ajax')) {
- $c->detach('ajax', [ 'reports/_problem-list.html' ]);
+ my $ajax_template = $c->stash->{ajax_template} || 'reports/_problem-list.html';
+ $c->detach('ajax', [ $ajax_template ]);
}
$c->stash->{rss_url} = '/rss/reports/' . $body_short;
@@ -167,16 +165,15 @@ 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', 'extra' ],
+ columns => [ 'id', 'category', 'extra' ],
distinct => 1,
order_by => [ 'category' ],
} )->all;
$c->stash->{filter_categories} = \@categories;
$c->stash->{filter_category} = { map { $_ => 1 } $c->get_param_list('filter_category', 1) };
- my $pins = $c->stash->{pins};
+ my $pins = $c->stash->{pins} || [];
- $c->stash->{page} = 'reports'; # So the map knows to make clickable pins
my %map_params = (
latitude => @$pins ? $pins->[0]{latitude} : 0,
longitude => @$pins ? $pins->[0]{longitude} : 0,
@@ -223,7 +220,7 @@ sub rss_area_ward : Path('/rss/area') : Args(2) {
return if $c->cobrand->reports_body_check( $c, $area );
# We must now have a string to check on mapit
- my $areas = mySociety::MaPit::call( 'areas', $area,
+ my $areas = FixMyStreet::MapIt::call( 'areas', $area,
type => $c->cobrand->area_types,
);
@@ -394,13 +391,14 @@ sub ward_check : Private {
$parent_id = $c->stash->{area}->{id};
}
- my $qw = mySociety::MaPit::call('area/children', [ $parent_id ],
+ my $qw = FixMyStreet::MapIt::call('area/children', [ $parent_id ],
type => $c->cobrand->area_types_children,
);
- my %names = map { $_ => 1 } @wards;
+ my %names = map { $c->cobrand->short_name({ name => $_ }) => 1 } @wards;
my @areas;
foreach my $area (sort { $a->{name} cmp $b->{name} } values %$qw) {
- push @areas, $area if $names{$area->{name}};
+ my $name = $c->cobrand->short_name($area);
+ push @areas, $area if $names{$name};
}
if (@areas) {
$c->stash->{ward} = $areas[0] if @areas == 1;
@@ -453,7 +451,7 @@ sub summary : Private {
# required to stop errors in generate_grouped_data
$c->stash->{q_state} = '';
- $c->stash->{ward} = $c->get_param('area');
+ $c->stash->{ward} = [ $c->get_param('area') || () ];
$c->stash->{start_date} = $dtf->format_date($start_date);
$c->stash->{end_date} = $c->get_param('end_date');
@@ -465,7 +463,7 @@ sub summary : Private {
$c->forward('/admin/fetch_contacts');
$c->stash->{contacts} = [ $c->stash->{contacts}->all ];
- $c->forward('/dashboard/construct_rs_filter');
+ $c->forward('/dashboard/construct_rs_filter', []);
if ( $c->get_param('csv') ) {
$c->detach('export_summary_csv');
@@ -481,7 +479,7 @@ sub export_summary_csv : Private {
my ( $self, $c ) = @_;
$c->stash->{csv} = {
- problems => $c->stash->{problems_rs}->search_rs({}, {
+ objects => $c->stash->{objects_rs}->search_rs({}, {
rows => 100,
order_by => { '-desc' => 'me.confirmed' },
}),
@@ -557,16 +555,12 @@ sub load_and_group_problems : Private {
my $states = $c->stash->{filter_problem_states};
my $where = {
- state => [ keys %$states ]
+ 'me.state' => [ keys %$states ]
};
- my $body = $c->stash->{body}; # Might be undef
+ $c->forward('check_non_public_reports_permission', [ $where ] );
- 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 $body = $c->stash->{body}; # Might be undef
my $filter = {
order_by => $c->stash->{sort_order},
@@ -620,12 +614,21 @@ sub load_and_group_problems : Private {
$where->{longitude} = { '>=', $min_lon, '<', $max_lon };
}
- $problems = $problems->search(
- $where,
- $filter
- )->include_comment_counts->page( $page );
+ my $cobrand_problems = $c->cobrand->call_hook('munge_load_and_group_problems', $where, $filter);
+
+ # JS will request the same (or more) data client side
+ return if $c->get_param('js');
- $c->stash->{pager} = $problems->pager;
+ if ($cobrand_problems) {
+ $problems = $cobrand_problems;
+ } else {
+ $problems = $problems->search(
+ $where,
+ $filter
+ )->include_comment_counts->page( $page );
+
+ $c->stash->{pager} = $problems->pager;
+ }
my ( %problems, @pins );
while ( my $problem = $problems->next ) {
@@ -633,19 +636,11 @@ sub load_and_group_problems : Private {
add_row( $c, $problem, 0, \%problems, \@pins );
next;
}
- if ( !$problem->bodies_str ) {
- # Problem was not sent to any body, add to all possible areas XXX
- my $a = $problem->areas; # Store, as otherwise is looked up every iteration.
- while ($a =~ /,(\d+)(?=,)/g) {
- add_row( $c, $problem, $1, \%problems, \@pins );
- }
- } else {
- # Add to bodies it was sent to
- my $bodies = $problem->bodies_str_ids;
- foreach ( @$bodies ) {
- next if $_ != $body->id;
- add_row( $c, $problem, $_, \%problems, \@pins );
- }
+ # Add to bodies it was sent to
+ my $bodies = $problem->bodies_str_ids;
+ foreach ( @$bodies ) {
+ next if $_ != $body->id;
+ add_row( $c, $problem, $_, \%problems, \@pins );
}
}
@@ -657,6 +652,34 @@ sub load_and_group_problems : Private {
return 1;
}
+
+sub check_non_public_reports_permission : Private {
+ my ($self, $c, $where) = @_;
+
+ if ( $c->user_exists ) {
+ my $user_has_permission;
+
+ if ( $c->user->is_super_user ) {
+ $user_has_permission = 1;
+ } else {
+ my $body = $c->stash->{body};
+
+ $user_has_permission = $body && (
+ $c->user->has_permission_to('report_inspect', $body->id) ||
+ $c->user->has_permission_to('report_mark_private', $body->id)
+ );
+ }
+
+ if ( $user_has_permission ) {
+ $where->{non_public} = 1 if $c->stash->{only_non_public};
+ } else {
+ $where->{non_public} = 0;
+ }
+ } else {
+ $where->{non_public} = 0;
+ }
+}
+
sub redirect_index : Private {
my ( $self, $c ) = @_;
my $url = '/reports';
@@ -678,7 +701,7 @@ sub stash_report_filter_status : Private {
my ( $self, $c ) = @_;
my @status = $c->get_param_list('status', 1);
- @status = ($c->cobrand->on_map_default_status) unless @status;
+ @status = ($c->stash->{page} eq 'my' ? 'all' : $c->cobrand->on_map_default_status) unless @status;
my %status = map { $_ => 1 } @status;
my %filter_problem_states;
@@ -729,6 +752,10 @@ sub stash_report_filter_status : Private {
}
}
+ if ($status{non_public}) {
+ $c->stash->{only_non_public} = 1;
+ }
+
if (keys %filter_problem_states == 0) {
my $s = FixMyStreet::DB::Result::Problem->open_states();
%filter_problem_states = (%filter_problem_states, %$s);
@@ -758,10 +785,13 @@ sub stash_report_sort : Private {
$sort =~ /^(updated|created|comments)-(desc|asc)$/;
my $order_by = $types{$1} || $1;
+ # field to use for report age cutoff
+ $c->stash->{report_age_field} = $order_by eq 'comment_count' ? 'lastupdate' : $order_by;
my $dir = $2;
$order_by = { -desc => $order_by } if $dir eq 'desc';
$c->stash->{sort_order} = $order_by;
+
return 1;
}
@@ -779,7 +809,8 @@ sub ajax : Private {
my @pins = map {
my $p = $_;
- [ $p->{latitude}, $p->{longitude}, $p->{colour}, $p->{id}, $p->{title} ]
+ # lat, lon, 'colour', ID, title, type/size, draggable
+ [ $p->{latitude}, $p->{longitude}, $p->{colour}, $p->{id}, $p->{title}, '', JSON->false ]
} @{$c->stash->{pins}};
my $list_html = $c->render_fragment($template);
diff --git a/perllib/FixMyStreet/App/Controller/Root.pm b/perllib/FixMyStreet/App/Controller/Root.pm
index 7f70623ae..340c930c2 100644
--- a/perllib/FixMyStreet/App/Controller/Root.pm
+++ b/perllib/FixMyStreet/App/Controller/Root.pm
@@ -39,6 +39,7 @@ sub auto : Private {
# decide which cobrand this request should use
$c->setup_request();
+ $c->detach('/auth/redirect') if $c->cobrand->call_hook('check_login_disallowed');
return 1;
}
diff --git a/perllib/FixMyStreet/App/Controller/Rss.pm b/perllib/FixMyStreet/App/Controller/Rss.pm
index e1da4445d..443e45b93 100755
--- a/perllib/FixMyStreet/App/Controller/Rss.pm
+++ b/perllib/FixMyStreet/App/Controller/Rss.pm
@@ -11,7 +11,7 @@ use FixMyStreet::App::Model::PhotoSet;
use FixMyStreet::Gaze;
use mySociety::Locale;
-use mySociety::MaPit;
+use FixMyStreet::MapIt;
use Lingua::EN::Inflect qw(ORD);
BEGIN { extends 'Catalyst::Controller'; }
@@ -66,7 +66,7 @@ sub reports_in_area : LocalRegex('^area/(\d+)$') {
my ( $self, $c ) = @_;
my $id = $c->req->captures->[0];
- my $area = mySociety::MaPit::call('area', $id);
+ my $area = FixMyStreet::MapIt::call('area', $id);
$c->stash->{type} = 'area_problems';
$c->stash->{qs} = '/' . $id;
$c->stash->{db_params} = [ $id ];
diff --git a/perllib/FixMyStreet/App/Controller/Tokens.pm b/perllib/FixMyStreet/App/Controller/Tokens.pm
index bb6140e0a..659d763de 100644
--- a/perllib/FixMyStreet/App/Controller/Tokens.pm
+++ b/perllib/FixMyStreet/App/Controller/Tokens.pm
@@ -28,17 +28,6 @@ problem but are not logged in.
sub confirm_problem : Path('/P') {
my ( $self, $c, $token_code ) = @_;
- if ($token_code eq '_test_') {
- $c->stash->{report} = {
- id => 123,
- title => 'Title of Report',
- bodies_str => '1',
- url => '/report/123',
- service => $c->get_param('service'),
- };
- return;
- }
-
my $auth_token =
$c->forward( 'load_auth_token', [ $token_code, 'problem' ] );
@@ -88,11 +77,6 @@ alert but are not logged in.
sub confirm_alert : Path('/A') {
my ( $self, $c, $token_code ) = @_;
- if ($token_code eq '_test_') {
- $c->stash->{confirm_type} = $c->get_param('confirm_type');
- return;
- }
-
my $auth_token = $c->forward( 'load_auth_token', [ $token_code, 'alert' ] );
# Load the alert
@@ -134,16 +118,6 @@ update but are not logged in.
sub confirm_update : Path('/C') {
my ( $self, $c, $token_code ) = @_;
- if ($token_code eq '_test_') {
- $c->stash->{problem} = {
- id => 123,
- title => 'Title of Report',
- bodies_str => '1',
- url => '/report/123',
- };
- return;
- }
-
my $auth_token =
$c->forward( 'load_auth_token', [ $token_code, 'comment' ] );