aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App.pm48
-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
-rw-r--r--perllib/FixMyStreet/App/Model/PhotoSet.pm140
-rw-r--r--perllib/FixMyStreet/Cobrand/Angus.pm132
-rw-r--r--perllib/FixMyStreet/Cobrand/BathNES.pm85
-rw-r--r--perllib/FixMyStreet/Cobrand/Borsetshire.pm4
-rw-r--r--perllib/FixMyStreet/Cobrand/Bristol.pm11
-rw-r--r--perllib/FixMyStreet/Cobrand/Bromley.pm301
-rw-r--r--perllib/FixMyStreet/Cobrand/Buckinghamshire.pm246
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm70
-rw-r--r--perllib/FixMyStreet/Cobrand/FiksGataMi.pm11
-rw-r--r--perllib/FixMyStreet/Cobrand/FixMyStreet.pm7
-rw-r--r--perllib/FixMyStreet/Cobrand/FixaMinGata.pm11
-rw-r--r--perllib/FixMyStreet/Cobrand/Greenwich.pm9
-rw-r--r--perllib/FixMyStreet/Cobrand/Lewisham.pm15
-rw-r--r--perllib/FixMyStreet/Cobrand/Lincolnshire.pm126
-rw-r--r--perllib/FixMyStreet/Cobrand/Northamptonshire.pm105
-rw-r--r--perllib/FixMyStreet/Cobrand/Oxfordshire.pm73
-rw-r--r--perllib/FixMyStreet/Cobrand/Rutland.pm10
-rw-r--r--perllib/FixMyStreet/Cobrand/UK.pm35
-rw-r--r--perllib/FixMyStreet/Cobrand/UKCouncils.pm150
-rw-r--r--perllib/FixMyStreet/Cobrand/Warwickshire.pm8
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm231
-rw-r--r--perllib/FixMyStreet/DB/Factories.pm34
-rw-r--r--perllib/FixMyStreet/DB/RABXColumn.pm12
-rw-r--r--perllib/FixMyStreet/DB/Result/Abuse.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/AdminLog.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Alert.pm21
-rw-r--r--perllib/FixMyStreet/DB/Result/AlertSent.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/AlertType.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Body.pm60
-rw-r--r--perllib/FixMyStreet/DB/Result/BodyArea.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Comment.pm128
-rw-r--r--perllib/FixMyStreet/DB/Result/Contact.pm31
-rw-r--r--perllib/FixMyStreet/DB/Result/ContactDefectType.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/ContactResponsePriority.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/ContactResponseTemplate.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/ContactsHistory.pm14
-rw-r--r--perllib/FixMyStreet/DB/Result/DefectType.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm161
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm149
-rw-r--r--perllib/FixMyStreet/DB/Result/Questionnaire.pm26
-rw-r--r--perllib/FixMyStreet/DB/Result/ReportExtraField.pm (renamed from perllib/FixMyStreet/DB/Result/ReportExtraFields.pm)12
-rw-r--r--perllib/FixMyStreet/DB/Result/ResponsePriority.pm14
-rw-r--r--perllib/FixMyStreet/DB/Result/ResponseTemplate.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Secret.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Session.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/State.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Token.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Translation.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/User.pm104
-rw-r--r--perllib/FixMyStreet/DB/Result/UserBodyPermission.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/UserPlannedReport.pm10
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Body.pm87
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Nearby.pm23
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Problem.pm43
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/ReportExtraField.pm (renamed from perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm)2
-rw-r--r--perllib/FixMyStreet/DateRange.pm72
-rw-r--r--perllib/FixMyStreet/Email.pm1
-rw-r--r--perllib/FixMyStreet/Geocode.pm2
-rw-r--r--perllib/FixMyStreet/Geocode/OSM.pm2
-rw-r--r--perllib/FixMyStreet/ImageMagick.pm69
-rw-r--r--perllib/FixMyStreet/Map.pm24
-rw-r--r--perllib/FixMyStreet/Map/Angus.pm18
-rw-r--r--perllib/FixMyStreet/Map/BathNES.pm6
-rw-r--r--perllib/FixMyStreet/Map/Bing.pm2
-rw-r--r--perllib/FixMyStreet/Map/Bristol.pm2
-rw-r--r--perllib/FixMyStreet/Map/Bromley.pm3
-rw-r--r--perllib/FixMyStreet/Map/Buckinghamshire.pm3
-rw-r--r--perllib/FixMyStreet/Map/FMS.pm8
-rw-r--r--perllib/FixMyStreet/Map/Google.pm1
-rw-r--r--perllib/FixMyStreet/Map/Lincolnshire.pm21
-rw-r--r--perllib/FixMyStreet/Map/OSM.pm7
-rw-r--r--perllib/FixMyStreet/Map/WMTSBase.pm6
-rw-r--r--perllib/FixMyStreet/Map/Zurich.pm4
-rw-r--r--perllib/FixMyStreet/MapIt.pm18
-rw-r--r--perllib/FixMyStreet/PhotoStorage.pm41
-rw-r--r--perllib/FixMyStreet/PhotoStorage/FileSystem.pm112
-rw-r--r--perllib/FixMyStreet/PhotoStorage/S3.pm122
-rw-r--r--perllib/FixMyStreet/Roles/Abuser.pm3
-rw-r--r--perllib/FixMyStreet/Roles/ConfirmValidation.pm38
-rw-r--r--perllib/FixMyStreet/Roles/Moderation.pm47
-rw-r--r--perllib/FixMyStreet/Roles/PhotoSet.pm5
-rw-r--r--perllib/FixMyStreet/Script/Alerts.pm11
-rw-r--r--perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm53
-rw-r--r--perllib/FixMyStreet/Script/Questionnaires.pm9
-rw-r--r--perllib/FixMyStreet/Script/Reports.pm31
-rwxr-xr-xperllib/FixMyStreet/Script/UpdateAllReports.pm38
-rw-r--r--perllib/FixMyStreet/SendReport.pm1
-rw-r--r--perllib/FixMyStreet/SendReport/Angus.pm167
-rw-r--r--perllib/FixMyStreet/SendReport/Blackhole.pm20
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm20
-rw-r--r--perllib/FixMyStreet/SendReport/Email/Highways.pm11
-rw-r--r--perllib/FixMyStreet/SendReport/Email/SingleBodyOnly.pm28
-rw-r--r--perllib/FixMyStreet/SendReport/Email/TfL.pm11
-rw-r--r--perllib/FixMyStreet/SendReport/Open311.pm3
-rw-r--r--perllib/FixMyStreet/Template.pm2
-rw-r--r--perllib/FixMyStreet/TestMech.pm60
123 files changed, 5170 insertions, 2819 deletions
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm
index 82fcce508..36f736cd2 100644
--- a/perllib/FixMyStreet/App.pm
+++ b/perllib/FixMyStreet/App.pm
@@ -10,10 +10,12 @@ use Memcached;
use FixMyStreet::Map;
use FixMyStreet::Email;
use FixMyStreet::Email::Sender;
+use FixMyStreet::PhotoStorage;
use Utils;
use Path::Tiny 'path';
use Try::Tiny;
+use Text::CSV;
use URI;
use URI::QueryParam;
@@ -34,6 +36,11 @@ our $VERSION = '0.01';
__PACKAGE__->config(
+ # Use REQUEST_URI, not PATH_INFO, to infer path. This fixes an issue
+ # with slashes in category names in admin (as PATH_INFO can't tell
+ # the difference between / and %2F)
+ use_request_uri_for_path => 1,
+
# get the config from the core object
%{ FixMyStreet->config() },
@@ -127,11 +134,9 @@ after 'prepare_headers' => sub {
__PACKAGE__->log->disable('debug') #
unless __PACKAGE__->debug;
-# Check upload_dir
-my $cache_dir = path(FixMyStreet->config('UPLOAD_DIR'))->absolute(FixMyStreet->path_to());
-$cache_dir->mkpath;
-unless ( -d $cache_dir && -w $cache_dir ) {
- warn "\x1b[31mCan't find/write to photo cache directory '$cache_dir'\x1b[0m\n";
+# Set up photo storage
+unless ( FixMyStreet::PhotoStorage::backend->init() ) {
+ warn "\x1b[31mCan't set up photo storage backend\x1b[0m\n";
}
=head1 NAME
@@ -237,9 +242,9 @@ sub setup_request {
$c->stash->{map_js} = FixMyStreet::Map::map_javascript();
unless ( FixMyStreet->config('MAPIT_URL') ) {
- my $port = $c->req->uri->port;
- $host = "$host:$port" unless $port == 80;
- mySociety::MaPit::configure( "http://$host/fakemapit/" );
+ my $host_port = $c->req->uri->host_port;
+ my $scheme = $c->req->uri->scheme;
+ mySociety::MaPit::configure( "$scheme://$host_port/fakemapit/" );
}
$c->stash->{has_fixed_state} = FixMyStreet::DB::Result::Problem::fixed_states->{fixed};
@@ -420,27 +425,6 @@ sub uri_with {
return $uri;
}
-=head2 uri_for
-
- $uri = $c->uri_for( ... );
-
-Like C<uri_for> except that it passes the uri to the cobrand to be altered if
-needed.
-
-=cut
-
-sub uri_for {
- my $c = shift;
- my @args = @_;
-
- my $uri = $c->next::method(@args);
-
- my $cobranded_uri = $c->cobrand->uri($uri);
-
- # note that the returned uri may be a string not an object (eg cities)
- return $cobranded_uri;
-}
-
=head2 uri_for_email
$uri = $c->uri_for_email( ... );
@@ -517,7 +501,11 @@ sub get_param_list {
my $value = $c->req->params->{$param};
return () unless defined $value;
my @value = ref $value ? @$value : ($value);
- return map { split /,/, $_ } @value if $allow_commas;
+ if ($allow_commas) {
+ my $csv = Text::CSV->new;
+ $csv->parse(join ',', @value);
+ @value = $csv->fields;
+ }
return @value;
}
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' ] );
diff --git a/perllib/FixMyStreet/App/Model/PhotoSet.pm b/perllib/FixMyStreet/App/Model/PhotoSet.pm
index 8fcc1700e..58b352c73 100644
--- a/perllib/FixMyStreet/App/Model/PhotoSet.pm
+++ b/perllib/FixMyStreet/App/Model/PhotoSet.pm
@@ -3,21 +3,18 @@ package FixMyStreet::App::Model::PhotoSet;
# TODO this isn't a Cat model, rename to something else
use Moose;
-use Path::Tiny 'path';
-
-my $IM = eval {
- require Image::Magick;
- Image::Magick->import;
- 1;
-};
use Scalar::Util 'openhandle', 'blessed';
-use Digest::SHA qw(sha1_hex);
use Image::Size;
use IPC::Cmd qw(can_run);
use IPC::Open3;
use MIME::Base64;
+use FixMyStreet;
+use FixMyStreet::ImageMagick;
+use FixMyStreet::PhotoStorage;
+
+# Attached Catalyst app, if present, for feeding back errors during photo upload
has c => (
is => 'ro',
);
@@ -57,27 +54,28 @@ has data_items => ( # either a) split from db_data or b) provided by photo uploa
my $self = shift;
my $data = $self->db_data or return [];
- return [$data] if (detect_type($data));
+ return [$data] if ($self->storage->detect_type($data));
return [ split ',' => $data ];
},
);
-has upload_dir => (
+has storage => (
is => 'ro',
lazy => 1,
default => sub {
- path(FixMyStreet->config('UPLOAD_DIR'))->absolute(FixMyStreet->path_to());
- },
+ return FixMyStreet::PhotoStorage::backend;
+ }
);
-sub detect_type {
- return 'jpeg' if $_[0] =~ /^\x{ff}\x{d8}/;
- return 'png' if $_[0] =~ /^\x{89}\x{50}/;
- return 'tiff' if $_[0] =~ /^II/;
- return 'gif' if $_[0] =~ /^GIF/;
- return '';
-}
+has symlinkable => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $cfg = FixMyStreet->config('PHOTO_STORAGE_OPTIONS');
+ return $cfg ? $cfg->{SYMLINK_FULL_SIZE} : 0;
+ }
+);
=head2 C<ids>, C<num_images>, C<get_id>, C<all_ids>
@@ -166,25 +164,21 @@ has ids => ( # Arrayref of $fileid tuples (always, so post upload/raw data proc
return ();
}
- # we have an image we can use - save it to the upload dir for storage
- my $fileid = $self->get_fileid($photo_blob);
- my $file = $self->get_file($fileid, $type);
- $upload->copy_to( $file );
- return $file->basename;
-
+ # we have an image we can use - save it to storage
+ $photo_blob = FixMyStreet::ImageMagick->new(blob => $photo_blob)->shrink('2048x2048')->as_blob;
+ return $self->storage->store_photo($photo_blob);
}
- if (my $type = detect_type($part)) {
+
+ # It might be a raw file stored in the DB column...
+ if (my $type = $self->storage->detect_type($part)) {
my $photo_blob = $part;
- my $fileid = $self->get_fileid($photo_blob);
- my $file = $self->get_file($fileid, $type);
- $file->spew_raw($photo_blob);
- return $file->basename;
+ return $self->storage->store_photo($photo_blob);
+ # TODO: Should this update the DB record with a pointer to the
+ # newly-stored file, instead of leaving it in the DB?
}
- my ($fileid, $type) = split /\./, $part;
- $type ||= 'jpeg';
- if ($fileid && length($fileid) == 40) {
- my $file = $self->get_file($fileid, $type);
- $file->basename;
+
+ if (my $key = $self->storage->validate_key($part)) {
+ $key;
} else {
# A bad hash, probably a bot spamming with bad data.
();
@@ -194,25 +188,13 @@ has ids => ( # Arrayref of $fileid tuples (always, so post upload/raw data proc
},
);
-sub get_fileid {
- my ($self, $photo_blob) = @_;
- return sha1_hex($photo_blob);
-}
-
-sub get_file {
- my ($self, $fileid, $type) = @_;
- my $cache_dir = $self->upload_dir;
- return path( $cache_dir, "$fileid.$type" );
-}
-
sub get_raw_image {
my ($self, $index) = @_;
my $filename = $self->get_id($index);
- my ($fileid, $type) = split /\./, $filename;
- my $file = $self->get_file($fileid, $type);
- if ($file->exists) {
- my $photo = $file->slurp_raw;
+ my ($photo, $type, $object) = $self->storage->retrieve_photo($filename);
+ if ($photo) {
return {
+ $object ? (object => $object) : (),
data => $photo,
content_type => "image/$type",
extension => $type,
@@ -229,14 +211,21 @@ sub get_image_data {
my $photo = $image->{data};
my $size = $args{size};
+
+ if ($self->symlinkable && $image->{object} && $size eq 'full') {
+ $image->{symlink} = delete $image->{object};
+ return $image;
+ }
+
+ my $im = FixMyStreet::ImageMagick->new(blob => $photo);
if ( $size eq 'tn' ) {
- $photo = _shrink( $photo, 'x100' );
+ $photo = $im->shrink('x100')->as_blob;
} elsif ( $size eq 'fp' ) {
- $photo = _crop( $photo );
+ $photo = $im->crop->as_blob;
} elsif ( $size eq 'full' ) {
# do nothing
} else {
- $photo = _shrink( $photo, $args{default} || '250x250' );
+ $photo = $im->shrink($args{default} || '250x250')->as_blob;
}
return {
@@ -298,7 +287,7 @@ sub rotate_image {
return if $index > $#images;
my $image = $self->get_raw_image($index);
- $images[$index] = _rotate_image( $image->{data}, $direction );
+ $images[$index] = FixMyStreet::ImageMagick->new(blob => $image->{data})->rotate($direction)->as_blob;
my $new_set = (ref $self)->new({
data_items => \@images,
@@ -310,47 +299,4 @@ sub rotate_image {
return $new_set->data; # e.g. new comma-separated fileid
}
-sub _rotate_image {
- my ($photo, $direction) = @_;
- return $photo unless $IM;
- my $image = Image::Magick->new;
- $image->BlobToImage($photo);
- my $err = $image->Rotate($direction);
- return 0 if $err;
- my @blobs = $image->ImageToBlob();
- undef $image;
- return $blobs[0];
-}
-
-
-# Shrinks a picture to the specified size, but keeping in proportion.
-sub _shrink {
- my ($photo, $size) = @_;
- return $photo unless $IM;
- my $image = Image::Magick->new;
- $image->BlobToImage($photo);
- my $err = $image->Scale(geometry => "$size>");
- throw Error::Simple("resize failed: $err") if "$err";
- $image->Strip();
- my @blobs = $image->ImageToBlob();
- undef $image;
- return $blobs[0];
-}
-
-# Shrinks a picture to 90x60, cropping so that it is exactly that.
-sub _crop {
- my ($photo) = @_;
- return $photo unless $IM;
- my $image = Image::Magick->new;
- $image->BlobToImage($photo);
- my $err = $image->Resize( geometry => "90x60^" );
- throw Error::Simple("resize failed: $err") if "$err";
- $err = $image->Extent( geometry => '90x60', gravity => 'Center' );
- throw Error::Simple("resize failed: $err") if "$err";
- $image->Strip();
- my @blobs = $image->ImageToBlob();
- undef $image;
- return $blobs[0];
-}
-
1;
diff --git a/perllib/FixMyStreet/Cobrand/Angus.pm b/perllib/FixMyStreet/Cobrand/Angus.pm
deleted file mode 100644
index 87dcc1d96..000000000
--- a/perllib/FixMyStreet/Cobrand/Angus.pm
+++ /dev/null
@@ -1,132 +0,0 @@
-package FixMyStreet::Cobrand::Angus;
-use parent 'FixMyStreet::Cobrand::UKCouncils';
-
-use strict;
-use warnings;
-
-sub council_area_id { return 2550; }
-sub council_area { return 'Angus'; }
-sub council_name { return 'Angus Council'; }
-sub council_url { return 'angus'; }
-
-sub base_url {
- my $self = shift;
- return $self->next::method() if FixMyStreet->config('STAGING_SITE');
- return 'https://fix.angus.gov.uk';
-}
-
-sub enter_postcode_text {
- my ($self) = @_;
- return 'Enter an Angus postcode, or street name and area';
-}
-
-sub example_places {
- return ( 'DD8 3AP', "Canmore Street" );
-}
-
-sub map_type { 'Angus' }
-
-sub default_show_name { 0 }
-
-sub disambiguate_location {
- my $self = shift;
- my $string = shift;
-
- return {
- %{ $self->SUPER::disambiguate_location() },
- town => 'Angus',
- centre => '56.7240845983561,-2.91774391131183',
- span => '0.525195055746977,0.985870680170788',
- bounds => [ 56.4616875530489, -3.40703662677109, 56.9868826087959, -2.4211659466003 ],
- };
-}
-
-sub pin_colour {
- my ( $self, $p, $context ) = @_;
- return 'grey' if $p->state eq 'not responsible';
- return 'green' if $p->is_fixed || $p->is_closed;
- return 'red' if $p->state eq 'confirmed';
- return 'yellow';
-}
-
-sub contact_email {
- my $self = shift;
- return join( '@', 'accessline', 'angus.gov.uk' );
-}
-
-=head2 temp_email_to_update, temp_update_contacts
-
-Temporary helper routines to update the extra for potholes (temporary setup
-hack, cargo-culted from Harrogate, may in future be superseded either by
-Open311/integration or a better mechanism for manually creating rich contacts).
-
-Can run with a script or command line like:
-
- bin/cron-wrapper perl -MFixMyStreet::App -MFixMyStreet::Cobrand::Angus -e \
- 'FixMyStreet::Cobrand::Angus->new({c => FixMyStreet::App->new})->temp_update_contacts'
-
-=cut
-
-sub temp_update_contacts {
- my $self = shift;
-
- my $contact_rs = $self->{c}->model('DB::Contact');
-
- my $body = FixMyStreet::DB->resultset('Body')->for_areas($self->council_area_id)->first;
-
- my $_update = sub {
- my ($category, $field, $category_details) = @_;
- # NB: we're accepting just 1 field, but supply as array [ $field ]
-
- my $contact = $contact_rs->find_or_create(
- {
- body => $body,
- category => $category,
- %{ $category_details || {} },
- },
- {
- key => 'contacts_body_id_category_idx'
- }
- );
-
- my %default = (
- variable => 'true',
- order => '1',
- required => 'no',
- datatype => 'string',
- datatype_description => 'a string',
- );
-
- if ($field->{datatype} || '' eq 'boolean') {
- my $description = $field->{description};
- %default = (
- %default,
- datatype => 'singlevaluelist',
- datatype_description => 'Yes or No',
- values => { value => [
- { key => ['No'], name => ['No'] },
- { key => ['Yes'], name => ['Yes'] },
- ] },
- );
- }
-
- $contact->update({
- # XXX: we're just setting extra with the expected layout,
- # this could be encapsulated more nicely
- extra => { _fields => [ { %default, %$field } ] },
- confirmed => 1,
- deleted => 0,
- editor => 'automated script',
- whenedited => \'NOW()',
- note => 'Edited by script as per requirements Jan 2016',
- });
- };
-
- $_update->( 'Street lighting', {
- code => 'column_id',
- description => 'Lamp post number',
- });
-
-}
-
-1;
diff --git a/perllib/FixMyStreet/Cobrand/BathNES.pm b/perllib/FixMyStreet/Cobrand/BathNES.pm
index 81080bed9..773edd3c3 100644
--- a/perllib/FixMyStreet/Cobrand/BathNES.pm
+++ b/perllib/FixMyStreet/Cobrand/BathNES.pm
@@ -4,6 +4,9 @@ use parent 'FixMyStreet::Cobrand::Whitelabel';
use strict;
use warnings;
+use Moo;
+with 'FixMyStreet::Roles::ConfirmValidation';
+
use LWP::Simple;
use URI;
use Try::Tiny;
@@ -19,10 +22,7 @@ sub contact_email {
return join( '@', 'councilconnect_rejections', 'bathnes.gov.uk' );
}
-sub update_email {
- my $self = shift;
- return join( '@', 'highways', 'bathnes.gov.uk' );
-}
+sub suggest_duplicates { 1 }
sub admin_user_domain { 'bathnes.gov.uk' }
@@ -34,6 +34,8 @@ sub base_url {
sub map_type { 'BathNES' }
+sub on_map_default_status { 'open' }
+
sub example_places {
return ( 'BA1 1JQ', "Lansdown Grove" );
}
@@ -91,13 +93,12 @@ sub send_questionnaires { 0 }
sub enable_category_groups { 1 }
-sub default_show_name { 0 }
-
sub default_map_zoom { 3 }
sub map_js_extra {
- my ($self, $c) = @_;
+ my $self = shift;
+ my $c = $self->{c};
return unless $c->user_exists;
my $banes_user = $c->user->from_body && $c->user->from_body->areas->{$self->council_area_id};
@@ -152,7 +153,7 @@ sub available_permissions {
return $permissions;
}
-sub report_sent_confirmation_email { 1 }
+sub report_sent_confirmation_email { 'id' }
sub lookup_usrn {
my $self = shift;
@@ -207,10 +208,60 @@ sub categories_restriction {
'me.send_method' => undef, # Open311 categories
'me.send_method' => '', # Open311 categories that have been edited in the admin
'me.send_method' => 'Email::BathNES', # Street Light Fault
+ 'me.send_method' => 'Blackhole', # Parks categories
] } );
}
-sub dashboard_export_add_columns {
+# Do a manual prefetch, as easier than sorting out quoting 'user'
+sub _dashboard_user_lookup {
+ my $self = shift;
+ my $c = $self->{c};
+
+ # Fetch all the relevant user IDs, and look them up
+ my @user_ids = $c->stash->{objects_rs}->search({}, { columns => [ 'user_id' ] })->all;
+ @user_ids = map { $_->user_id } @user_ids;
+ @user_ids = $c->model('DB::User')->search(
+ { id => { -in => \@user_ids } },
+ { columns => [ 'id', 'email', 'phone' ] })->all;
+
+ # Plus all staff users for contributed_by lookup
+ push @user_ids, $c->model('DB::User')->search(
+ { from_body => { '!=' => undef } },
+ { columns => [ 'id', 'email', 'phone' ] })->all;
+
+ my %user_lookup = map { $_->id => { email => $_->email, phone => $_->phone } } @user_ids;
+ return \%user_lookup;
+}
+
+sub dashboard_export_updates_add_columns {
+ my $self = shift;
+ my $c = $self->{c};
+
+ return unless $c->user->has_body_permission_to('export_extra_columns');
+
+ push @{$c->stash->{csv}->{headers}}, "Staff User";
+ push @{$c->stash->{csv}->{headers}}, "User Email";
+ push @{$c->stash->{csv}->{columns}}, "staff_user";
+ push @{$c->stash->{csv}->{columns}}, "user_email";
+
+ my $user_lookup = $self->_dashboard_user_lookup;
+
+ $c->stash->{csv}->{extra_data} = sub {
+ my $report = shift;
+
+ my $staff_user = '';
+ if ( my $contributed_by = $report->get_extra_metadata('contributed_by') ) {
+ $staff_user = $user_lookup->{$contributed_by}{email};
+ }
+
+ return {
+ user_email => $user_lookup->{$report->user_id}{email} || '',
+ staff_user => $staff_user,
+ };
+ };
+}
+
+sub dashboard_export_problems_add_columns {
my $self = shift;
my $c = $self->{c};
@@ -220,39 +271,33 @@ sub dashboard_export_add_columns {
@{ $c->stash->{csv}->{headers} },
"User Email",
"User Phone",
- "Reported As",
"Staff User",
"Attribute Data",
- "Site Used",
];
$c->stash->{csv}->{columns} = [
@{ $c->stash->{csv}->{columns} },
"user_email",
"user_phone",
- "reported_as",
"staff_user",
"attribute_data",
- "site_used",
];
+ my $user_lookup = $self->_dashboard_user_lookup;
+
$c->stash->{csv}->{extra_data} = sub {
my $report = shift;
- my $reported_as = $report->get_extra_metadata('contributed_as') || '';
my $staff_user = '';
if ( my $contributed_by = $report->get_extra_metadata('contributed_by') ) {
- $staff_user = $c->model('DB::User')->find({ id => $contributed_by })->email;
+ $staff_user = $user_lookup->{$contributed_by}{email};
}
- my $site_used = $report->service || $report->cobrand || '';
my $attribute_data = join "; ", map { $_->{name} . " = " . $_->{value} } @{ $report->get_extra_fields };
return {
- user_email => $report->user->email || '',
- user_phone => $report->user->phone || '',
- reported_as => $reported_as,
+ user_email => $user_lookup->{$report->user_id}{email} || '',
+ user_phone => $user_lookup->{$report->user_id}{phone} || '',
staff_user => $staff_user,
attribute_data => $attribute_data,
- site_used => $site_used,
};
};
}
diff --git a/perllib/FixMyStreet/Cobrand/Borsetshire.pm b/perllib/FixMyStreet/Cobrand/Borsetshire.pm
index d9b018d69..e721bee0f 100644
--- a/perllib/FixMyStreet/Cobrand/Borsetshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Borsetshire.pm
@@ -31,4 +31,8 @@ sub send_questionnaires {
sub bypass_password_checks { 1 }
+sub enable_category_groups { 1 }
+
+sub suggest_duplicates { 1 }
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Bristol.pm b/perllib/FixMyStreet/Cobrand/Bristol.pm
index 25dc5ab0a..fa2d3fabb 100644
--- a/perllib/FixMyStreet/Cobrand/Bristol.pm
+++ b/perllib/FixMyStreet/Cobrand/Bristol.pm
@@ -23,8 +23,6 @@ sub map_type {
'Bristol';
}
-sub default_link_zoom { 6 }
-
sub disambiguate_location {
my $self = shift;
my $string = shift;
@@ -77,4 +75,13 @@ sub open311_config {
$params->{always_send_email} = 1;
}
+sub open311_contact_meta_override {
+ my ($self, $service, $contact, $meta) = @_;
+
+ my %server_set = (easting => 1, northing => 1);
+ foreach (@$meta) {
+ $_->{automated} = 'server_set' if $server_set{$_->{code}};
+ }
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm
index 5d14d0b01..341fb6a30 100644
--- a/perllib/FixMyStreet/Cobrand/Bromley.pm
+++ b/perllib/FixMyStreet/Cobrand/Bromley.pm
@@ -3,20 +3,48 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
+use utf8;
use DateTime::Format::W3CDTF;
+use DateTime::Format::Flexible;
+use Try::Tiny;
+use FixMyStreet::DateRange;
sub council_area_id { return 2482; }
sub council_area { return 'Bromley'; }
sub council_name { return 'Bromley Council'; }
sub council_url { return 'bromley'; }
+sub report_validation {
+ my ($self, $report, $errors) = @_;
+
+ if ( length( $report->detail ) > 1750 ) {
+ $errors->{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), 1750 );
+ }
+
+ return $errors;
+}
+
+# This makes sure that the subcategory Open311 attribute question is
+# also stored in the report's subcategory column. This could be done
+# in process_open311_extras, but seemed easier to keep that separate
+sub report_new_munge_before_insert {
+ my ($self, $report) = @_;
+
+ $report->subcategory($report->get_extra_field_value('service_sub_code'));
+}
+
sub base_url {
my $self = shift;
return $self->next::method() if FixMyStreet->config('STAGING_SITE');
return 'https://fix.bromley.gov.uk';
}
-sub default_show_name { 0 }
+sub problems_on_map_restriction {
+ my ($self, $rs) = @_;
+ return $rs if FixMyStreet->staging_flag('skip_checks');
+ my $tfl = FixMyStreet::DB->resultset('Body')->search({ name => 'TfL' })->first;
+ return $rs->to_body($tfl ? [ $self->body->id, $tfl->id ] : $self->body);
+}
sub disambiguate_location {
my $self = shift;
@@ -30,18 +58,24 @@ sub disambiguate_location {
# a different Priory Avenue in Petts Wood
# From Google maps search, "BR6 0PL" is a valid postcode for Old Priory Avenue
if ($string =~/^old\s+priory\s+av\w*$/i) {
- $string = 'Ramsden Road';
- $town = ', BR6 0PL';
+ $town = 'BR6 0PL';
}
# White Horse Hill is on boundary with Greenwich, so need a
# specific postcode
- $string = 'BR7 6DH' if $string =~ /^white\s+horse/i;
+ $town = 'BR7 6DH' if $string =~ /^white\s+horse/i;
$town = '' if $string =~ /orpington/i;
+ $string =~ s/(, *)?br[12]$//i;
+ $town = 'Beckenham' if $string =~ s/(, *)?br3$//i;
+ $town = 'West Wickham' if $string =~ s/(, *)?br4$//i;
+ $town = 'Orpington' if $string =~ s/(, *)?br[56]$//i;
+ $town = 'Chislehurst' if $string =~ s/(, *)?br7$//i;
+ $town = 'Swanley' if $string =~ s/(, *)?br8$//i;
return {
%{ $self->SUPER::disambiguate_location() },
+ string => $string,
town => $town,
centre => '51.366836,0.040623',
span => '0.154963,0.24347',
@@ -64,6 +98,7 @@ sub map_type {
# Bromley pins always yellow
sub pin_colour {
my ( $self, $p, $context ) = @_;
+ return 'grey' if !$self->owns_problem( $p );
return 'yellow';
}
@@ -92,6 +127,8 @@ sub contact_email {
}
sub contact_name { 'Bromley Council (do not reply)'; }
+sub abuse_reports_only { 1; }
+
sub reports_per_page { return 20; }
sub tweak_all_reports_map {
@@ -104,6 +141,39 @@ sub tweak_all_reports_map {
$c->stash->{map}->{any_zoom} = 0;
$c->stash->{map}->{zoom} = 11;
}
+
+ # A place where this can happen
+ return unless $c->stash->{template} && $c->stash->{template} eq 'about/heatmap.html';
+
+ my $children = $c->stash->{body}->first_area_children;
+ foreach (values %$children) {
+ $_->{url} = $c->uri_for( $c->stash->{body_url}
+ . '/' . $c->cobrand->short_name( $_ )
+ );
+ }
+ $c->stash->{children} = $children;
+
+ my %subcats = $self->subcategories;
+ my $filter = $c->stash->{filter_categories};
+ my @new_contacts;
+ foreach (@$filter) {
+ push @new_contacts, $_;
+ foreach (@{$subcats{$_->id}}) {
+ push @new_contacts, {
+ category => $_->{key},
+ category_display => (" " x 4) . $_->{name},
+ };
+ }
+ }
+ $c->stash->{filter_categories} = \@new_contacts;
+
+ if (!%{$c->stash->{filter_category}}) {
+ my $cats = $c->user->categories;
+ my $subcats = $c->user->get_extra_metadata('subcategories') || [];
+ $c->stash->{filter_category} = { map { $_ => 1 } @$cats, @$subcats } if @$cats || @$subcats;
+ }
+
+ $c->stash->{ward_hash} = { map { $_->{id} => 1 } @{$c->stash->{wards}} } if $c->stash->{wards};
}
sub title_list {
@@ -117,6 +187,7 @@ sub open311_config {
my $title = $row->title;
foreach (@$extra) {
+ next unless $_->{value};
$title .= ' | ID: ' . $_->{value} if $_->{name} eq 'feature_id';
$title .= ' | PROW ID: ' . $_->{value} if $_->{name} eq 'prow_reference';
}
@@ -150,5 +221,227 @@ sub open311_config {
$params->{extended_description} = 0;
}
+sub open311_config_updates {
+ my ($self, $params) = @_;
+ $params->{endpoints} = {
+ service_request_updates => 'update.xml',
+ update => 'update.xml'
+ };
+}
+
+sub open311_pre_send {
+ my ($self, $row, $open311) = @_;
+
+ my $extra = $row->extra || {};
+ unless ( $extra->{title} ) {
+ $extra->{title} = $row->user->title;
+ $row->extra( $extra );
+ }
+}
+
+sub open311_munge_update_params {
+ my ($self, $params, $comment, $body) = @_;
+ delete $params->{update_id};
+ $params->{public_anonymity_required} = $comment->anonymous ? 'TRUE' : 'FALSE',
+ $params->{update_id_ext} = $comment->id;
+ $params->{service_request_id_ext} = $comment->problem->id;
+}
+
+sub open311_contact_meta_override {
+ my ($self, $service, $contact, $meta) = @_;
+
+ $contact->set_extra_metadata( id_field => 'service_request_id_ext');
+
+ my %server_set = (easting => 1, northing => 1, service_request_id_ext => 1);
+ foreach (@$meta) {
+ $_->{automated} = 'server_set' if $server_set{$_->{code}};
+ }
+
+ # Lights we want to store feature ID, PROW on all categories.
+ push @$meta, {
+ code => 'prow_reference',
+ datatype => 'string',
+ description => 'Right of way reference',
+ order => 101,
+ required => 'false',
+ variable => 'true',
+ automated => 'hidden_field',
+ };
+ push @$meta, {
+ code => 'feature_id',
+ datatype => 'string',
+ description => 'Feature ID',
+ order => 100,
+ required => 'false',
+ variable => 'true',
+ automated => 'hidden_field',
+ } if $service->{service_code} eq 'SLRS';
+
+ my @override = qw(
+ requested_datetime
+ report_url
+ title
+ last_name
+ email
+ report_title
+ public_anonymity_required
+ email_alerts_requested
+ );
+ my %ignore = map { $_ => 1 } @override;
+ @$meta = grep { !$ignore{$_->{code}} } @$meta;
+}
+
+# If any subcategories ticked in user edit admin, make sure they're saved.
+sub admin_user_edit_extra_data {
+ my $self = shift;
+ my $c = $self->{c};
+ my $user = $c->stash->{user};
+
+ return unless $c->get_param('submit') && $user && $user->from_body;
+
+ $c->stash->{body} = $user->from_body;
+ my %subcats = $self->subcategories;
+ my @subcat_ids = map { $_->{key} } map { @$_ } values %subcats;
+ my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @subcat_ids;
+ $user->set_extra_metadata('subcategories', \@new_contact_ids);
+}
+
+# Returns a hash of contact ID => list of subcategories
+# (which are stored as Open311 attribute questions)
+sub subcategories {
+ my $self = shift;
+
+ my @c = $self->body->contacts->not_deleted->all;
+ my %subcategories;
+ foreach my $contact (@c) {
+ my @fields = @{$contact->get_extra_fields};
+ my ($field) = grep { $_->{code} eq 'service_sub_code' } @fields;
+ $subcategories{$contact->id} = $field->{values} || [];
+ }
+ return %subcategories;
+}
+
+# Returns the list of categories, with Bromley subcategories added,
+# for the user edit admin interface
+sub add_admin_subcategories {
+ my $self = shift;
+ my $c = $self->{c};
+
+ my $user = $c->stash->{user};
+ my @subcategories = @{$user->get_extra_metadata('subcategories') || []};
+ my %active_contacts = map { $_ => 1 } @subcategories;
+
+ my %subcats = $self->subcategories;
+ my $contacts = $c->stash->{contacts};
+ my @new_contacts;
+ foreach (@$contacts) {
+ push @new_contacts, $_;
+ foreach (@{$subcats{$_->{id}}}) {
+ push @new_contacts, {
+ id => $_->{key},
+ category => ("&nbsp;" x 4) . $_->{name},
+ active => $active_contacts{$_->{key}},
+ };
+ }
+ }
+ return \@new_contacts;
+}
+
+sub about_hook {
+ my $self = shift;
+ my $c = $self->{c};
+
+ # Display a special custom dashboard page, with heatmap
+ if ($c->stash->{template} eq 'about/heatmap.html') {
+ $c->forward('/dashboard/check_page_allowed');
+ # We want a special sidebar
+ $c->stash->{ajax_template} = "about/heatmap-list.html";
+ $c->set_param('js', 1) unless $c->get_param('ajax'); # Want to load pins client-side
+ $c->forward('/reports/body', [ 'Bromley' ]);
+ }
+}
+
+# On heatmap page, include querying on subcategories, wards, dates, provided
+sub munge_load_and_group_problems {
+ my ($self, $where, $filter) = @_;
+ my $c = $self->{c};
+
+ return unless $c->stash->{template} && $c->stash->{template} eq 'about/heatmap.html';
+
+ if (!$where->{category}) {
+ my $cats = $c->user->categories;
+ my $subcats = $c->user->get_extra_metadata('subcategories') || [];
+ $where->{category} = [ @$cats, @$subcats ] if @$cats || @$subcats;
+ }
+
+ my %subcats = $self->subcategories;
+ my $subcat;
+ my %chosen = map { $_ => 1 } @{$where->{category} || []};
+ my @subcat = grep { $chosen{$_} } map { $_->{key} } map { @$_ } values %subcats;
+ if (@subcat) {
+ my %chosen = map { $_ => 1 } @subcat;
+ $where->{'-or'} = {
+ category => [ grep { !$chosen{$_} } @{$where->{category}} ],
+ subcategory => \@subcat,
+ };
+ delete $where->{category};
+ }
+
+ # Wards
+ my @areas = @{$c->user->area_ids || []};
+ # Want to get everything if nothing given in an ajax call
+ if (!$c->stash->{wards} && @areas) {
+ $c->stash->{wards} = [ map { { id => $_ } } @areas ];
+ $where->{areas} = [
+ map { { 'like', '%,' . $_ . ',%' } } @areas
+ ];
+ }
+
+ # Date range
+ my $start_default = DateTime->today(time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone)->subtract(months => 1);
+ $c->stash->{start_date} = $c->get_param('start_date') || $start_default->strftime('%Y-%m-%d');
+ $c->stash->{end_date} = $c->get_param('end_date');
+
+ my $range = FixMyStreet::DateRange->new(
+ start_date => $c->stash->{start_date},
+ start_default => $start_default,
+ end_date => $c->stash->{end_date},
+ formatter => $c->model('DB')->storage->datetime_parser,
+ );
+ $where->{'me.confirmed'} = $range->sql;
+
+ delete $filter->{rows};
+
+ # Load the relevant stuff for the sidebar as well
+ my $problems = $self->problems->search($where, $filter);
+
+ $c->stash->{five_newest} = [ $problems->search(undef, {
+ rows => 5,
+ order_by => { -desc => 'confirmed' },
+ })->all ];
+
+ $c->stash->{ten_oldest} = [ $problems->search({
+ 'me.state' => [ FixMyStreet::DB::Result::Problem->open_states() ],
+ }, {
+ rows => 10,
+ order_by => 'lastupdate',
+ })->all ];
+
+ my $params = { map { my $n = $_; s/me\./problem\./; $_ => $where->{$n} } keys %$where };
+ my @c = $c->model('DB::Comment')->to_body($self->body)->search({
+ %$params,
+ 'me.user_id' => { -not_in => [ $c->user->id, $self->body->comment_user_id ] },
+ 'me.state' => 'confirmed',
+ }, {
+ columns => 'problem_id',
+ group_by => 'problem_id',
+ order_by => { -desc => \'max(me.confirmed)' },
+ rows => 5,
+ })->all;
+ $c->stash->{five_commented} = [ map { $_->problem } @c ];
+
+ return $problems;
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm
index a5e45d5a9..3a33d6f58 100644
--- a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm
@@ -4,16 +4,15 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-use LWP::Simple;
-use URI;
-use Try::Tiny;
-use JSON::MaybeXS;
+use Moo;
+with 'FixMyStreet::Roles::ConfirmValidation';
sub council_area_id { return 2217; }
sub council_area { return 'Buckinghamshire'; }
sub council_name { return 'Buckinghamshire County Council'; }
sub council_url { return 'buckinghamshire'; }
+
sub example_places {
return ( 'HP19 7QF', "Walton Road" );
}
@@ -43,6 +42,8 @@ sub disambiguate_location {
};
}
+sub on_map_default_status { 'open' }
+
sub pin_colour {
my ( $self, $p, $context ) = @_;
return 'grey' if $p->state eq 'not responsible';
@@ -51,6 +52,8 @@ sub pin_colour {
return 'yellow';
}
+sub admin_user_domain { 'buckscc.gov.uk' }
+
sub contact_email {
my $self = shift;
return join( '@', 'fixmystreetbs', 'email.buckscc.gov.uk' );
@@ -87,12 +90,152 @@ sub open311_config {
$row->set_extra_fields(@$extra);
}
+sub open311_pre_send {
+ my ($self, $row, $open311) = @_;
+
+ return unless $row->extra;
+ my $extra = $row->get_extra_fields;
+ if (@$extra) {
+ @$extra = grep { $_->{name} ne 'road-placement' } @$extra;
+ $row->set_extra_fields(@$extra);
+ }
+}
+
+sub open311_post_send {
+ my ($self, $row, $h) = @_;
+
+ # Check Open311 was successful
+ return unless $row->external_id;
+
+ # For certain categories, send an email also
+ my $addresses = {
+ 'Flytipping' => [ join('@', 'illegaldumpingcosts', $self->admin_user_domain), "TfB" ],
+ 'Blocked drain' => [ join('@', 'floodmanagement', $self->admin_user_domain), "Flood Management" ],
+ 'Ditch issue' => [ join('@', 'floodmanagement', $self->admin_user_domain), "Flood Management" ],
+ 'Flooded subway' => [ join('@', 'floodmanagement', $self->admin_user_domain), "Flood Management" ],
+ };
+ my $dest = $addresses->{$row->category};
+ return unless $dest;
+
+ my $sender = FixMyStreet::SendReport::Email->new( to => [ $dest ] );
+ $sender->send($row, $h);
+}
+
+sub open311_config_updates {
+ my ($self, $params) = @_;
+ $params->{mark_reopen} = 1;
+}
+
+sub open311_contact_meta_override {
+ my ($self, $service, $contact, $meta) = @_;
+
+ push @$meta, {
+ code => 'road-placement',
+ datatype => 'singlevaluelist',
+ description => 'Is the fly-tip located on',
+ order => 100,
+ required => 'true',
+ variable => 'true',
+ values => [
+ { key => 'road', name => 'The road' },
+ { key => 'off-road', name => 'Off the road/on a verge' },
+ ],
+ } if $service->{service_name} eq 'Flytipping';
+}
+
+sub process_open311_extras {
+ my ($self, $c, $body, $extra) = @_;
+
+ return unless $c->stash->{report}; # Don't care about updates
+
+ $self->flytipping_body_fix(
+ $c->stash->{report},
+ $c->get_param('road-placement'),
+ $c->stash->{field_errors},
+ );
+}
+
+sub flytipping_body_fix {
+ my ($self, $report, $road_placement, $errors) = @_;
+
+ return unless $report->category eq 'Flytipping';
+
+ if ($report->bodies_str =~ /,/) {
+ # Sent to both councils in the area
+ my @bodies = values %{$report->bodies};
+ my $county = (grep { $_->name =~ /^Buckinghamshire/ } @bodies)[0];
+ my $district = (grep { $_->name !~ /^Buckinghamshire/ } @bodies)[0];
+ # Decide which to send to based upon the answer to the extra question:
+ if ($road_placement eq 'road') {
+ $report->bodies_str($county->id);
+ } elsif ($road_placement eq 'off-road') {
+ $report->bodies_str($district->id);
+ }
+ } else {
+ # If the report is only being sent to the district, we do
+ # not care about the road question, if it is missing
+ if (!$report->to_body_named('Buckinghamshire')) {
+ delete $errors->{'road-placement'};
+ }
+ }
+}
+
+sub filter_report_description {
+ my ($self, $description) = @_;
+
+ # this allows _ in the domain name but I figure it's unlikely to
+ # generate false positives so lets go with that for the same of
+ # a simpler regex
+ $description =~ s/\b[\w.!#$%&'*+\-\/=?^_{|}~]+\@[\w\-]+\.[^ ]+\b//g;
+ $description =~ s/ (?: \+ \d{2} \s? | \b 0 ) (?:
+ \d{2} \s? \d{4} \s? \d{4} # 0xx( )xxxx( )xxxx
+ | \d{3} \s \d{3} \s? \d{4} # 0xxx xxx( )xxxx
+ | \d{3} \s? \d{2} \s \d{4,5} # 0xxx( )xx xxxx(x)
+ | \d{4} \s \d{5,6} # 0xxxx xxxxx(x)
+ ) \b //gx;
+
+ return $description;
+}
+
sub map_type { 'Buckinghamshire' }
sub default_map_zoom { 3 }
sub enable_category_groups { 1 }
+sub _dashboard_export_add_columns {
+ my $self = shift;
+ my $c = $self->{c};
+
+ push @{$c->stash->{csv}->{headers}}, "Staff User";
+ push @{$c->stash->{csv}->{columns}}, "staff_user";
+
+ # All staff users, for contributed_by lookup
+ my @user_ids = $c->model('DB::User')->search(
+ { from_body => $self->body->id },
+ { columns => [ 'id', 'email', ] })->all;
+ my %user_lookup = map { $_->id => $_->email } @user_ids;
+
+ $c->stash->{csv}->{extra_data} = sub {
+ my $report = shift;
+ my $staff_user = '';
+ if (my $contributed_by = $report->get_extra_metadata('contributed_by')) {
+ $staff_user = $user_lookup{$contributed_by};
+ }
+ return {
+ staff_user => $staff_user,
+ };
+ };
+}
+
+sub dashboard_export_updates_add_columns {
+ shift->_dashboard_export_add_columns;
+}
+
+sub dashboard_export_problems_add_columns {
+ shift->_dashboard_export_add_columns;
+}
+
# Enable adding/editing of parish councils in the admin
sub add_extra_areas {
my ($self, $areas) = @_;
@@ -304,7 +447,7 @@ sub should_skip_sending_update {
sub disable_phone_number_entry { 1 }
-sub report_sent_confirmation_email { 1 }
+sub report_sent_confirmation_email { 'external_id' }
sub is_council_with_case_management { 1 }
@@ -315,88 +458,25 @@ sub categories_restriction {
my ($self, $rs) = @_;
# Buckinghamshire is a two-tier council, but only want to display
# county-level categories on their cobrand.
- return $rs->search( { 'body.id' => 2217 } );
+ return $rs->search( [ { 'body_areas.area_id' => 2217 }, { category => 'Flytipping' } ], { join => { body => 'body_areas' } });
}
-sub lookup_site_code {
- my $self = shift;
- my $row = shift;
-
- my $buffer = 200; # metres
- my ($x, $y) = $row->local_coords;
- my ($w, $s, $e, $n) = ($x-$buffer, $y-$buffer, $x+$buffer, $y+$buffer);
-
- my $uri = URI->new("https://tilma.mysociety.org/mapserver/bucks");
- $uri->query_form(
- REQUEST => "GetFeature",
- SERVICE => "WFS",
- SRSNAME => "urn:ogc:def:crs:EPSG::27700",
- TYPENAME => "Whole_Street",
- VERSION => "1.1.0",
- outputformat => "geojson",
- BBOX => "$w,$s,$e,$n"
- );
-
- my $response = get($uri);
-
- my $j = JSON->new->utf8->allow_nonref;
- try {
- $j = $j->decode($response);
- } catch {
- # There was either no asset found, or an error with the WFS
- # call - in either case let's just proceed without the USRN.
- return '';
- };
-
- # We have a list of features, and we want to find the one closest to the
- # report location.
- my $site_code = '';
- my $nearest;
-
- # There are only certain features we care about, the rest can be ignored.
- my @valid_types = ( "2", "3A", "3B", "4A", "4B", "HE", "HWOA", "HWSA", "P" );
- my %valid_types = map { $_ => 1 } @valid_types;
-
- for my $feature ( @{ $j->{features} } ) {
+sub lookup_site_code_config { {
+ buffer => 200, # metres
+ url => "https://tilma.mysociety.org/mapserver/bucks",
+ srsname => "urn:ogc:def:crs:EPSG::27700",
+ typename => "Whole_Street",
+ property => "site_code",
+ accept_feature => sub {
+ my $feature = shift;
+
+ # There are only certain features we care about, the rest can be ignored.
+ my @valid_types = ( "2", "3A", "3B", "4A", "4B", "HE", "HWOA", "HWSA", "P" );
+ my %valid_types = map { $_ => 1 } @valid_types;
my $type = $feature->{properties}->{feature_ty};
- next unless $valid_types{$type};
-
- # We shouldn't receive anything aside from these two geometry types, but belt and braces.
- next unless $feature->{geometry}->{type} eq 'MultiLineString' || $feature->{geometry}->{type} eq 'LineString';
- my @coordinates = @{ $feature->{geometry}->{coordinates} };
- if ( $feature->{geometry}->{type} eq 'MultiLineString') {
- # The coordinates are stored as a list of lists, so flatten 'em out
- @coordinates = map { @{ $_ } } @coordinates;
- }
-
- # If any of this feature's points are closer than those we've seen so
- # far then use the site_code from this feature.
- for my $coords ( @coordinates ) {
- my ($fx, $fy) = @$coords;
- my $distance = $self->_distance($x, $y, $fx, $fy);
- if ( !defined $nearest || $distance < $nearest ) {
- $site_code = $feature->{properties}->{site_code};
- $nearest = $distance;
- }
- }
+ return $valid_types{$type};
}
-
- return $site_code;
-}
-
-
-=head2 _distance
-
-Returns the cartesian distance between two coordinates.
-This is not a general-purpose distance function, it's intended for use with
-fairly nearby coordinates in EPSG:27700 where a spheroid doesn't need to be
-taken into account.
-
-=cut
-sub _distance {
- my ($self, $ax, $ay, $bx, $by) = @_;
- return sqrt( (($ax - $bx) ** 2) + (($ay - $by) ** 2) );
-}
+} }
1;
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index 816c5e315..a8146128e 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -13,7 +13,6 @@ use URI;
use Digest::MD5 qw(md5_hex);
use Carp;
-use mySociety::MaPit;
use mySociety::PostcodeUtil;
=head1 The default cobrand
@@ -237,6 +236,18 @@ sub base_url_for_report {
return $self->base_url_with_lang;
}
+=item relative_url_for_report
+
+Returns the relative base url for a report (might be different in a two-tier
+county, but normally blank). Report may be an object, or a hashref.
+
+=cut
+
+sub relative_url_for_report {
+ my ( $self, $report ) = @_;
+ return "";
+}
+
=item base_host
Return the base host for the cobranded version of the site
@@ -397,25 +408,6 @@ Return cobrand extra data for the problem
sub cobrand_data_for_generic_problem { '' }
-=item uri
-
-Given a URL ($_[1]), QUERY, EXTRA_DATA, return a URL with any extra params
-needed appended to it.
-
-In the default case, we need to make sure zoom is always present if lat/lon
-are, to stop OpenLayers defaulting to null/0.
-
-=cut
-
-sub uri {
- my ( $self, $uri ) = @_;
- $uri->query_param( zoom => $self->default_link_zoom )
- if $uri->query_param('lat') && !$uri->query_param('zoom');
-
- return $uri;
-}
-
-
=item header_params
Return any params to be added to responses
@@ -445,6 +437,10 @@ sub reports_per_page {
return FixMyStreet->config('ALL_REPORTS_PER_PAGE') || 100;
}
+sub report_age {
+ return '6 months';
+}
+
=item reports_ordering
The order_by clause to use for reports on all reports page
@@ -526,7 +522,7 @@ sub find_closest {
my $problem = $data->{problem};
my $lat = $problem ? $problem->latitude : $data->{latitude};
my $lon = $problem ? $problem->longitude : $data->{longitude};
- my $j = $problem->geocode if $problem;
+ my $j = $problem ? $problem->geocode : undef;
if (!$j) {
$j = FixMyStreet::Geocode::Bing::reverse( $lat, $lon,
@@ -713,12 +709,15 @@ sub available_permissions {
report_edit => _("Edit reports"),
report_edit_category => _("Edit report category"), # future use
report_edit_priority => _("Edit report priority"), # future use
+ report_mark_private => _("View/Mark private reports"),
report_inspect => _("Markup problem details"),
report_instruct => _("Instruct contractors to fix problems"), # future use
+ report_prefill => _("Automatically populate report subject/detail"),
planned_reports => _("Manage shortlist"),
contribute_as_another_user => _("Create reports/updates on a user's behalf"),
contribute_as_anonymous_user => _("Create reports/updates as anonymous user"),
contribute_as_body => _("Create reports/updates as the council"),
+ default_to_body => _("Default to creating reports/updates as the council"),
view_body_contribute_details => _("See user detail for reports created as the council"),
# NB this permission is special in that it can be assigned to users
@@ -761,6 +760,14 @@ used in emails).
sub contact_name { FixMyStreet->config('CONTACT_NAME') }
sub contact_email { FixMyStreet->config('CONTACT_EMAIL') }
+=item abuse_reports_only
+
+Return true if only abuse reports should be allowed from the contact form.
+
+=cut
+
+sub abuse_reports_only { 0; }
+
=item email_host
Return if we are the virtual host that sends email for this cobrand
@@ -987,18 +994,14 @@ sub tweak_all_reports_map {}
sub can_support_problems { return 0; }
-=item default_map_zoom / default_link_zoom
+=item default_map_zoom
default_map_zoom is used when displaying a map overriding the
default of max-4 or max-3 depending on population density.
-default_link_zoom is used in links that contain a 'lat' and no
-zoom, to stop e.g. OpenLayers defaulting to null/0.
-
=cut
sub default_map_zoom { undef };
-sub default_link_zoom { 3 }
sub users_can_hide { return 0; }
@@ -1008,9 +1011,7 @@ Returns true if the show name checkbox should be ticked by default.
=cut
-sub default_show_name {
- 1;
-}
+sub default_show_name { 0 }
=item report_check_for_errors
@@ -1029,7 +1030,7 @@ sub report_check_for_errors {
);
}
-sub report_sent_confirmation_email { 0; }
+sub report_sent_confirmation_email { '' }
=item never_confirm_reports
@@ -1180,6 +1181,7 @@ Return true if an Open311 service attribute should be a hidden field.
sub category_extra_hidden {
my ($self, $meta) = @_;
+ return 1 if ($meta->{automated} || '') eq 'hidden_field';
return 0;
}
@@ -1243,4 +1245,12 @@ still be sent (because it wasn't disabled on the FixMyStreet cobrand).
sub send_moderation_notifications { 1 }
+=item privacy_policy_url
+
+The URL of the privacy policy to use on the report and update submissions forms.
+
+=cut
+
+sub privacy_policy_url { '/privacy' }
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
index 4b95dfeaf..171faaa8b 100644
--- a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
+++ b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
@@ -135,6 +135,9 @@ sub council_rss_alert_options {
}
}
+ my $body_kommune = FixMyStreet::DB->resultset('Body')->for_areas($kommune->{id})->first;
+ my $body_fylke = FixMyStreet::DB->resultset('Body')->for_areas($fylke->{id})->first;
+
if ( $fylke->{id} == 3 ) { # Oslo
my $short_name = $self->short_name($fylke);
( my $id_name = $short_name ) =~ tr/+/_/;
@@ -142,7 +145,7 @@ sub council_rss_alert_options {
push @options,
{
type => 'council',
- id => sprintf( 'council:%s:%s', $fylke->{id}, $id_name ),
+ id => sprintf( 'council:%s:%s', $body_fylke->id, $id_name ),
rss_text =>
sprintf( _('RSS feed of problems within %s'), $fylke->{name} ),
text => sprintf( _('Problems within %s'), $fylke->{name} ),
@@ -177,7 +180,7 @@ sub council_rss_alert_options {
push @reported_to_options,
{
type => 'council',
- id => sprintf( 'council:%s:%s', $kommune->{id}, $id_kommune_name ),
+ id => sprintf( 'council:%s:%s', $body_kommune->id, $id_kommune_name ),
rss_text =>
sprintf( _('RSS feed of %s'), $kommune->{name} ),
text => $kommune->{name},
@@ -185,11 +188,11 @@ sub council_rss_alert_options {
},
{
type => 'council',
- id => sprintf( 'council:%s:%s', $fylke->{id}, $id_fylke_name ),
+ id => sprintf( 'council:%s:%s', $body_fylke->id, $id_fylke_name ),
rss_text =>
sprintf( _('RSS feed of %s'), $fylke->{name} ),
text => $fylke->{name},
- uri => $c->uri_for( '/rss/reports/', $short_fylke_name ),
+ uri => $c->uri_for( '/rss/reports', $short_fylke_name ),
};
}
diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
index 6c826ec01..fb454f495 100644
--- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
+++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
@@ -8,6 +8,10 @@ use mySociety::Random;
use constant COUNCIL_ID_BROMLEY => 2482;
+sub on_map_default_status { return 'open'; }
+
+sub enable_category_groups { 1 }
+
# Special extra
sub path_to_web_templates {
my $self = shift;
@@ -48,9 +52,6 @@ sub extra_contact_validation {
my $self = shift;
my $c = shift;
- # Don't care about dest if reporting abuse
- return () if $c->stash->{problem};
-
my %errors;
$c->stash->{dest} = $c->get_param('dest');
diff --git a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm
index 29e840dfa..d1a1980a7 100644
--- a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm
+++ b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm
@@ -181,4 +181,15 @@ sub state_groups_inspect {
]
}
+sub always_view_body_contribute_details {
+ return 1;
+}
+
+# Average responsiveness will only be calculated if a body
+# has at least this many fixed reports.
+# (Used in the Top 5 list in /reports)
+sub body_responsiveness_threshold {
+ return 5;
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Greenwich.pm b/perllib/FixMyStreet/Cobrand/Greenwich.pm
index 6ff30e83d..2aaa5d776 100644
--- a/perllib/FixMyStreet/Cobrand/Greenwich.pm
+++ b/perllib/FixMyStreet/Cobrand/Greenwich.pm
@@ -66,4 +66,13 @@ sub open311_config {
$row->set_extra_fields( @$extra );
}
+sub open311_contact_meta_override {
+ my ($self, $service, $contact, $meta) = @_;
+
+ my %server_set = (easting => 1, northing => 1, closest_address => 1);
+ foreach (@$meta) {
+ $_->{automated} = 'server_set' if $server_set{$_->{code}};
+ }
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Lewisham.pm b/perllib/FixMyStreet/Cobrand/Lewisham.pm
new file mode 100644
index 000000000..325f6e833
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Lewisham.pm
@@ -0,0 +1,15 @@
+package FixMyStreet::Cobrand::Lewisham;
+use base 'FixMyStreet::Cobrand::UK';
+
+use strict;
+use warnings;
+
+sub council_area_id { 2492 }
+
+sub open311_post_update_skip {
+ my ($self) = @_;
+ return 1;
+}
+
+1;
+
diff --git a/perllib/FixMyStreet/Cobrand/Lincolnshire.pm b/perllib/FixMyStreet/Cobrand/Lincolnshire.pm
new file mode 100644
index 000000000..8d8ba3268
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Lincolnshire.pm
@@ -0,0 +1,126 @@
+package FixMyStreet::Cobrand::Lincolnshire;
+use parent 'FixMyStreet::Cobrand::UKCouncils';
+
+use strict;
+use warnings;
+
+use LWP::Simple;
+use URI;
+use Try::Tiny;
+use JSON::MaybeXS;
+
+use Moo;
+with 'FixMyStreet::Roles::ConfirmValidation';
+
+sub council_area_id { return 2232; }
+sub council_area { return 'Lincolnshire'; }
+sub council_name { return 'Lincolnshire County Council'; }
+sub council_url { return 'lincolnshire'; }
+sub is_two_tier { 1 }
+
+sub enable_category_groups { 1 }
+sub send_questionnaires { 0 }
+sub report_sent_confirmation_email { 'external_id' }
+
+sub admin_user_domain { 'lincolnshire.gov.uk' }
+
+sub enter_postcode_text {
+ my ($self) = @_;
+ return 'Enter a Lincolnshire postcode, street name and area, or check an existing report number';
+}
+
+
+sub base_url {
+ my $self = shift;
+ return $self->next::method() if FixMyStreet->config('STAGING_SITE');
+ return 'https://fixmystreet.lincolnshire.gov.uk';
+}
+
+sub contact_email {
+ my $self = shift;
+ return join( '@', 'confirm_support', 'lincolnshire.gov.uk' );
+}
+
+
+sub example_places {
+ return ( 'LN1 1YL', 'Orchard Street, Lincoln' );
+}
+
+sub disambiguate_location {
+ my $self = shift;
+ my $string = shift;
+ return {
+ %{ $self->SUPER::disambiguate_location() },
+ town => 'Lincolnshire',
+ centre => '53.1128371079972,-0.237920757894981',
+ span => '0.976148231905086,1.17860658530345',
+ bounds => [ 52.6402179235688, -0.820651304784901, 53.6163661554738, 0.357955280518546 ],
+ };
+}
+
+
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ my $extra = $row->get_extra_fields;
+ push @$extra,
+ { name => 'report_url',
+ value => $h->{url} },
+ { name => 'title',
+ value => $row->title },
+ { name => 'description',
+ value => $row->detail };
+
+ # Reports made via FMS.com or the app probably won't have a site code
+ # value because we don't display the adopted highways layer on those
+ # frontends. Instead we'll look up the closest asset from the WFS
+ # service at the point we're sending the report over Open311.
+ if (!$row->get_extra_field_value('site_code')) {
+ if (my $site_code = $self->lookup_site_code($row)) {
+ push @$extra,
+ { name => 'site_code',
+ value => $site_code };
+ }
+ }
+
+ $row->set_extra_fields(@$extra);
+}
+
+sub lookup_site_code_config { {
+ buffer => 200, # metres
+ url => "https://tilma.mysociety.org/mapserver/lincs",
+ srsname => "urn:ogc:def:crs:EPSG::27700",
+ typename => "NSG",
+ property => "Site_Code",
+ accept_feature => sub { 1 }
+} }
+
+
+sub categories_restriction {
+ my ($self, $rs) = @_;
+ # Lincolnshire is a two-tier council, but don't want to display
+ # all district-level categories on their cobrand - just a couple.
+ return $rs->search( { -or => [
+ 'body.name' => "Lincolnshire County Council",
+
+ # District categories:
+ 'me.category' => { -in => [
+ 'Street nameplates',
+ 'Bench/cycle rack/litter bin/planter',
+ ] },
+ ] } );
+}
+
+sub map_type { 'Lincolnshire' }
+
+sub pin_colour {
+ my ( $self, $p, $context ) = @_;
+ my $ext_status = $p->get_extra_metadata('external_status_code');
+ return 'yellow' if $p->state eq 'confirmed' && $ext_status && $ext_status eq '0135';
+ return 'red' if $p->state eq 'confirmed';
+ return 'green' if $p->is_fixed || $p->is_closed;
+ return 'grey' if $p->state eq 'not responsible' || !$self->owns_problem( $p );
+ return 'yellow';
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/Northamptonshire.pm b/perllib/FixMyStreet/Cobrand/Northamptonshire.pm
new file mode 100644
index 000000000..683dc059c
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Northamptonshire.pm
@@ -0,0 +1,105 @@
+package FixMyStreet::Cobrand::Northamptonshire;
+use parent 'FixMyStreet::Cobrand::Whitelabel';
+
+use strict;
+use warnings;
+
+use Moo;
+with 'FixMyStreet::Roles::ConfirmValidation';
+
+sub council_area_id { 2234 }
+sub council_area { 'Northamptonshire' }
+sub council_name { 'Northamptonshire County Council' }
+sub council_url { 'northamptonshire' }
+
+sub example_places { ( 'NN1 1NS', "Bridge Street" ) }
+
+sub enter_postcode_text { 'Enter a Northamptonshire postcode, street name and area, or check an existing report number' }
+
+sub base_url {
+ my $self = shift;
+ return $self->next::method() if FixMyStreet->config('STAGING_SITE');
+ return 'https://fixmystreet.northamptonshire.gov.uk';
+}
+
+sub disambiguate_location {
+ my $self = shift;
+ my $string = shift;
+
+ return {
+ %{ $self->SUPER::disambiguate_location() },
+ centre => '52.30769080650276,-0.8647071378799923',
+ bounds => [ 51.97726778979222, -1.332346116362747, 52.643600776698605, -0.3416080408721255 ],
+ };
+}
+
+sub categories_restriction {
+ my ($self, $rs) = @_;
+ return $rs->search( [ { 'body.name' => 'Northamptonshire County Council' } ], { join => { body => 'body_areas' } });
+}
+
+sub send_questionnaires { 0 }
+
+sub on_map_default_status { 'open' }
+
+sub report_sent_confirmation_email { 'id' }
+
+sub problems_on_map_restriction {
+ my ($self, $rs) = @_;
+ # Northamptonshire don't want to show district/borough reports
+ # on the site
+ return $self->problems_restriction($rs);
+}
+
+sub contact_email {
+ my $self = shift;
+ return join( '@', 'highways', $self->council_url . '.gov.uk' );
+}
+
+sub privacy_policy_url {
+ 'https://www3.northamptonshire.gov.uk/councilservices/council-and-democracy/transparency/information-policies/privacy-notice/place/Pages/street-doctor.aspx'
+}
+
+sub enable_category_groups { 1 }
+
+sub is_two_tier { 1 }
+
+sub get_geocoder { 'OSM' }
+
+sub map_type { 'OSM' }
+
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ my $extra = $row->get_extra_fields;
+
+ # remove the emergency category which is informational only
+ @$extra = grep { $_->{name} ne 'emergency' } @$extra;
+
+ push @$extra,
+ { name => 'report_url',
+ value => $h->{url} },
+ { name => 'title',
+ value => $row->title },
+ { name => 'description',
+ value => $row->detail },
+ { name => 'category',
+ value => $row->category };
+
+ $row->set_extra_fields(@$extra);
+
+ $params->{multi_photos} = 1;
+}
+
+# sending updates not part of initial phase
+sub should_skip_sending_update { 1; }
+
+sub report_validation {
+ my ($self, $report, $errors) = @_;
+
+ if ( length( $report->title ) > 120 ) {
+ $errors->{title} = sprintf( _('Summaries are limited to %s characters in length. Please shorten your summary'), 120 );
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
index 479d9c43b..08482a0b3 100644
--- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
@@ -10,6 +10,28 @@ sub council_name { return 'Oxfordshire County Council'; }
sub council_url { return 'oxfordshire'; }
sub is_two_tier { return 1; }
+sub report_validation {
+ my ($self, $report, $errors) = @_;
+
+ if ( length( $report->detail ) > 1700 ) {
+ $errors->{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), 1700 );
+ }
+
+ if ( length( $report->name ) > 50 ) {
+ $errors->{name} = sprintf( 'Names are limited to %d characters in length.', 50 );
+ }
+
+ if ( length( $report->user->phone ) > 20 ) {
+ $errors->{phone} = sprintf( 'Phone numbers are limited to %s characters in length.', 20 );
+ }
+
+ if ( length( $report->user->email ) > 50 ) {
+ $errors->{username} = sprintf( 'Emails are limited to %s characters in length.', 50 );
+ }
+
+ return $errors;
+}
+
sub is_council_with_case_management {
# XXX Change this to return 1 when OCC FMSfC goes live.
return FixMyStreet->config('STAGING_SITE');
@@ -51,7 +73,21 @@ sub default_map_zoom { return 3; }
# let staff hide OCC reports
sub users_can_hide { return 1; }
-sub default_show_name { 0 }
+sub lookup_by_ref_regex {
+ return qr/^\s*((?:ENQ)?\d+)\s*$/;
+}
+
+sub lookup_by_ref {
+ my ($self, $ref) = @_;
+
+ if ( $ref =~ /^ENQ/ ) {
+ my $len = length($ref);
+ my $filter = "%T18:customer_reference,T$len:$ref,%";
+ return { 'extra' => { -like => $filter } };
+ }
+
+ return 0;
+}
=head2 problem_response_days
@@ -130,10 +166,10 @@ sub pin_hover_title {
sub state_groups_inspect {
[
- [ _('New'), [ 'confirmed', 'investigating' ] ],
- [ _('Scheduled'), [ 'action scheduled' ] ],
- [ _('Fixed'), [ 'fixed - council' ] ],
- [ _('Closed'), [ 'not responsible', 'duplicate', 'unable to fix' ] ],
+ [ 'New', [ 'confirmed', 'investigating' ] ],
+ [ 'Scheduled', [ 'action scheduled' ] ],
+ [ 'Fixed', [ 'fixed - council' ] ],
+ [ 'Closed', [ 'not responsible', 'duplicate', 'unable to fix' ] ],
]
}
@@ -142,22 +178,28 @@ sub open311_config {
my $extra = $row->get_extra_fields;
push @$extra, { name => 'external_id', value => $row->id };
+ push @$extra, { name => 'northing', value => $h->{northing} };
+ push @$extra, { name => 'easting', value => $h->{easting} };
if ($h->{closest_address}) {
push @$extra, { name => 'closest_address', value => "$h->{closest_address}" }
}
- if ( $row->used_map || ( !$row->used_map && !$row->postcode ) ) {
- push @$extra, { name => 'northing', value => $h->{northing} };
- push @$extra, { name => 'easting', value => $h->{easting} };
- }
$row->set_extra_fields( @$extra );
$params->{extended_description} = 'oxfordshire';
}
-sub open311_pre_send {
- my ($self, $row, $open311) = @_;
- $open311->endpoints( { requests => 'open311_service_request.cgi' } );
+sub open311_config_updates {
+ my ($self, $params) = @_;
+ $params->{use_customer_reference} = 1;
+}
+
+sub should_skip_sending_update {
+ my ($self, $update ) = @_;
+
+ # Oxfordshire stores the external id of the problem as a customer reference
+ # in metadata
+ return 1 if !$update->problem->get_extra_metadata('customer_reference');
}
sub on_map_default_status { return 'open'; }
@@ -234,6 +276,13 @@ sub available_permissions {
my $perms = $self->next::method();
$perms->{Bodies}->{defect_type_edit} = "Add/edit defect types";
+ delete $perms->{Problems}->{report_edit};
+ delete $perms->{Problems}->{report_edit_category};
+ delete $perms->{Problems}->{report_edit_priority};
+ delete $perms->{Problems}->{report_inspect};
+ delete $perms->{Problems}->{report_instruct};
+ delete $perms->{Problems}->{planned_reports};
+
return $perms;
}
diff --git a/perllib/FixMyStreet/Cobrand/Rutland.pm b/perllib/FixMyStreet/Cobrand/Rutland.pm
index 6993b0964..af635ac59 100644
--- a/perllib/FixMyStreet/Cobrand/Rutland.pm
+++ b/perllib/FixMyStreet/Cobrand/Rutland.pm
@@ -9,6 +9,16 @@ sub council_area { return 'Rutland'; }
sub council_name { return 'Rutland County Council'; }
sub council_url { return 'rutland'; }
+sub report_validation {
+ my ($self, $report, $errors) = @_;
+
+ if ( length( $report->name ) > 40 ) {
+ $errors->{name} = sprintf( _('Names are limited to %d characters in length.'), 40 );
+ }
+
+ return $errors;
+}
+
sub open311_config {
my ($self, $row, $h, $params) = @_;
diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm
index f99f29eb4..1c6ebe29a 100644
--- a/perllib/FixMyStreet/Cobrand/UK.pm
+++ b/perllib/FixMyStreet/Cobrand/UK.pm
@@ -190,9 +190,11 @@ sub council_rss_alert_options {
my ( @options, @reported_to_options );
if ( $num_councils == 1 or $num_councils == 2 ) {
my ($council, $ward);
+ my $body = FixMyStreet::DB->resultset('Body')->active->search({ name => { '!=' => 'TfL' } })->for_areas(keys %$all_areas)->first;
foreach (values %$all_areas) {
if ($councils{$_->{type}}) {
$council = $_;
+ $council->{id} = $body->id; # Want to use body ID, not MapIt area ID
$council->{short_name} = $self->short_name( $council );
( $council->{id_name} = $council->{short_name} ) =~ tr/+/_/;
} else {
@@ -247,6 +249,9 @@ sub council_rss_alert_options {
my $county_name = $county->{name};
my $c_ward_name = $c_ward->{name};
+ my $body_dis = FixMyStreet::DB->resultset('Body')->active->for_areas($district->{id})->first;
+ my $body_cty = FixMyStreet::DB->resultset('Body')->active->for_areas($county->{id})->first;
+
push @options, {
type => 'area',
id => sprintf( 'area:%s:%s', $district->{id}, $district->{id_name} ),
@@ -275,29 +280,32 @@ sub council_rss_alert_options {
push @reported_to_options, {
type => 'council',
- id => sprintf( 'council:%s:%s', $district->{id}, $district->{id_name} ),
+ id => sprintf( 'council:%s:%s', $body_dis->id, $district->{id_name} ),
text => sprintf( _('Reports sent to %s'), $district->{name} ),
rss_text => sprintf( _('RSS feed of %s'), $district->{name}),
uri => $c->uri_for( '/rss/reports/' . $district->{short_name} ),
}, {
type => 'ward',
- id => sprintf( 'ward:%s:%s:%s:%s', $district->{id}, $d_ward->{id}, $district->{id_name}, $d_ward->{id_name} ),
+ id => sprintf( 'ward:%s:%s:%s:%s', $body_dis->id, $d_ward->{id}, $district->{id_name}, $d_ward->{id_name} ),
rss_text => sprintf( _('RSS feed of %s, within %s ward'), $district->{name}, $d_ward->{name}),
text => sprintf( _('Reports sent to %s, within %s ward'), $district->{name}, $d_ward->{name}),
uri => $c->uri_for( '/rss/reports/' . $district->{short_name} . '/' . $d_ward->{short_name} ),
- }, {
+ }
+ if $body_dis;
+ push @reported_to_options, {
type => 'council',
- id => sprintf( 'council:%s:%s', $county->{id}, $county->{id_name} ),
+ id => sprintf( 'council:%s:%s', $body_cty->id, $county->{id_name} ),
text => sprintf( _('Reports sent to %s'), $county->{name} ),
rss_text => sprintf( _('RSS feed of %s'), $county->{name}),
uri => $c->uri_for( '/rss/reports/' . $county->{short_name} ),
}, {
type => 'ward',
- id => sprintf( 'ward:%s:%s:%s:%s', $county->{id}, $c_ward->{id}, $county->{id_name}, $c_ward->{id_name} ),
+ id => sprintf( 'ward:%s:%s:%s:%s', $body_cty->id, $c_ward->{id}, $county->{id_name}, $c_ward->{id_name} ),
rss_text => sprintf( _('RSS feed of %s, within %s ward'), $county->{name}, $c_ward->{name}),
text => sprintf( _('Reports sent to %s, within %s ward'), $county->{name}, $c_ward->{name}),
uri => $c->uri_for( '/rss/reports/' . $county->{short_name} . '/' . $c_ward->{short_name} ),
- };
+ }
+ if $body_cty;
} else {
throw Error::Simple('An area with three tiers of council? Impossible! '. join('|',keys %$all_areas));
@@ -323,13 +331,9 @@ sub report_check_for_errors {
);
}
- if ( $report->bodies_str && $report->detail ) {
- # Custom character limit:
- if ( $report->to_body_named('Bromley') && length($report->detail) > 1750 ) {
- $errors{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), 1750 );
- } elsif ( $report->to_body_named('Oxfordshire') && length($report->detail) > 1700 ) {
- $errors{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), 1700 );
- }
+ my $cobrand = $self->get_body_handler_for_problem($report);
+ if ( $cobrand->can('report_validation') ) {
+ $cobrand->report_validation( $report, \%errors );
}
return %errors;
@@ -351,7 +355,7 @@ sub get_body_handler_for_problem {
my ($self, $row) = @_;
my @bodies = values %{$row->bodies};
- my %areas = map { %{$_->areas} } @bodies;
+ my %areas = map { %{$_->areas} } grep { $_->name ne 'TfL' } @bodies;
my $cobrand = FixMyStreet::Cobrand->body_handler(\%areas);
return $cobrand if $cobrand;
@@ -393,8 +397,7 @@ sub lookup_by_ref_regex {
sub category_extra_hidden {
my ($self, $meta) = @_;
return 1 if $meta->{code} eq 'usrn' || $meta->{code} eq 'asset_id';
- return 1 if $meta->{automated} eq 'hidden_field';
- return 0;
+ return $self->SUPER::category_extra_hidden($meta);
}
1;
diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
index 753aa2404..1beafef73 100644
--- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm
+++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
@@ -6,6 +6,10 @@ use warnings;
use Carp;
use URI::Escape;
+use LWP::Simple;
+use URI;
+use Try::Tiny;
+use JSON::MaybeXS;
sub is_council {
1;
@@ -38,10 +42,11 @@ sub restriction {
return { cobrand => shift->moniker };
}
-# UK cobrands assume that each MapIt area ID maps both ways with one body
+# UK cobrands assume that each MapIt area ID maps both ways with one
+# body. Except TfL.
sub body {
my $self = shift;
- my $body = FixMyStreet::DB->resultset('Body')->for_areas($self->council_area_id)->first;
+ my $body = FixMyStreet::DB->resultset('Body')->for_areas($self->council_area_id)->search({ name => { '!=', 'TfL' } })->first;
return $body;
}
@@ -175,31 +180,35 @@ sub owns_problem {
} else { # Object
@bodies = values %{$report->bodies};
}
- my %areas = map { %{$_->areas} } @bodies;
+ # Want to ignore the TfL body that covers London councils
+ my %areas = map { %{$_->areas} } grep { $_->name ne 'TfL' } @bodies;
return $areas{$self->council_area_id} ? 1 : undef;
}
-# If the council is two-tier then show pins for the other council as grey
+# If the council is two-tier, or e.g. TfL reports,
+# then show pins for the other council as grey
sub pin_colour {
my ( $self, $p, $context ) = @_;
- return 'grey' if $self->is_two_tier && !$self->owns_problem( $p );
+ return 'grey' if !$self->owns_problem( $p );
return $self->next::method($p, $context);
}
-# If we ever link to a county problem report, needs to be to main FixMyStreet
+# If we ever link to a county problem report, or a TfL report,
+# needs to be to main FixMyStreet
sub base_url_for_report {
my ( $self, $report ) = @_;
- if ( $self->is_two_tier ) {
- if ( $self->owns_problem( $report ) ) {
- return $self->base_url;
- } else {
- return FixMyStreet->config('BASE_URL');
- }
- } else {
+ if ( $self->owns_problem( $report ) ) {
return $self->base_url;
+ } else {
+ return FixMyStreet->config('BASE_URL');
}
}
+sub relative_url_for_report {
+ my ( $self, $report ) = @_;
+ return $self->owns_problem($report) ? "" : FixMyStreet->config('BASE_URL');
+}
+
sub admin_allow_user {
my ( $self, $user ) = @_;
return 1 if $user->is_superuser;
@@ -211,6 +220,7 @@ sub available_permissions {
my $self = shift;
my $perms = $self->next::method();
+ $perms->{Problems}->{default_to_body} = "Default to creating reports/updates as " . $self->council_name;
$perms->{Problems}->{contribute_as_body} = "Create reports/updates as " . $self->council_name;
$perms->{Problems}->{view_body_contribute_details} = "See user detail for reports created as " . $self->council_name;
$perms->{Users}->{user_assign_areas} = "Assign users to areas in " . $self->council_name;
@@ -222,4 +232,118 @@ sub prefill_report_fields_for_inspector { 1 }
sub social_auth_disabled { 1 }
+=head2 lookup_site_code
+
+Reports made via FMS.com or the app probably won't have a site code
+value (required for Confirm integrations) because we don't display
+the adopted highways layer on those frontends.
+Instead we'll look up the closest asset from the WFS
+service at the point we're sending the report over Open311.
+
+NB this requires the cobrand to implement `lookup_site_code_config` -
+see Buckinghamshire or Lincolnshire for an example.
+
+
+=cut
+
+sub lookup_site_code {
+ my $self = shift;
+ my $row = shift;
+ my $buffer = shift;
+
+ my $cfg = $self->lookup_site_code_config;
+
+ $buffer ||= $cfg->{buffer}; # metres
+ my ($x, $y) = $row->local_coords;
+ my ($w, $s, $e, $n) = ($x-$buffer, $y-$buffer, $x+$buffer, $y+$buffer);
+
+ my $uri = URI->new($cfg->{url});
+ $uri->query_form(
+ REQUEST => "GetFeature",
+ SERVICE => "WFS",
+ SRSNAME => $cfg->{srsname},
+ TYPENAME => $cfg->{typename},
+ VERSION => "1.1.0",
+ outputformat => "geojson",
+ BBOX => "$w,$s,$e,$n"
+ );
+
+ my $response = get($uri);
+
+ my $j = JSON->new->utf8->allow_nonref;
+ try {
+ $j = $j->decode($response);
+ } catch {
+ # There was either no asset found, or an error with the WFS
+ # call - in either case let's just proceed without the USRN.
+ return '';
+ };
+
+ # We have a list of features, and we want to find the one closest to the
+ # report location.
+ my $site_code = '';
+ my $nearest;
+
+ for my $feature ( @{ $j->{features} } ) {
+ next unless $cfg->{accept_feature}($feature);
+
+ # We shouldn't receive anything aside from these two geometry types, but belt and braces.
+ next unless $feature->{geometry}->{type} eq 'MultiLineString' || $feature->{geometry}->{type} eq 'LineString';
+
+ my @coordinates = @{ $feature->{geometry}->{coordinates} };
+ if ( $feature->{geometry}->{type} eq 'MultiLineString') {
+ # The coordinates are stored as a list of lists, so flatten 'em out
+ @coordinates = map { @{ $_ } } @coordinates;
+ }
+
+ # If any of this feature's points are closer than those we've seen so
+ # far then use the site_code from this feature.
+ for my $coords ( @coordinates ) {
+ my ($fx, $fy) = @$coords;
+ my $distance = $self->_distance($x, $y, $fx, $fy);
+ if ( !defined $nearest || $distance < $nearest ) {
+ $site_code = $feature->{properties}->{$cfg->{property}};
+ $nearest = $distance;
+ }
+ }
+ }
+
+ return $site_code;
+}
+
+sub extra_contact_validation {
+ my $self = shift;
+ my $c = shift;
+
+ # Don't care about dest unless reporting abuse
+ return () unless $c->stash->{problem};
+
+ my %errors;
+
+ $c->stash->{dest} = $c->get_param('dest');
+
+ if (!$c->get_param('dest')) {
+ $errors{dest} = "Please enter a topic of your message";
+ } elsif ( $c->get_param('dest') eq 'council' || $c->get_param('dest') eq 'update' ) {
+ $errors{not_for_us} = 1;
+ }
+
+ return %errors;
+}
+
+
+=head2 _distance
+
+Returns the cartesian distance between two coordinates.
+This is not a general-purpose distance function, it's intended for use with
+fairly nearby coordinates in EPSG:27700 where a spheroid doesn't need to be
+taken into account.
+
+=cut
+sub _distance {
+ my ($self, $ax, $ay, $bx, $by) = @_;
+ return sqrt( (($ax - $bx) ** 2) + (($ay - $by) ** 2) );
+}
+
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Warwickshire.pm b/perllib/FixMyStreet/Cobrand/Warwickshire.pm
index 73f66f3da..c301450bc 100644
--- a/perllib/FixMyStreet/Cobrand/Warwickshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Warwickshire.pm
@@ -34,4 +34,12 @@ sub contact_name { 'Warwickshire County Council (do not reply)'; }
sub send_questionnaires { 0 }
+sub open311_contact_meta_override {
+ my ($self, $service, $contact, $meta) = @_;
+
+ $contact->set_extra_metadata( id_field => 'external_id');
+
+ @$meta = grep { $_->{code} ne 'closest_address' } @$meta;
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index b2a0e331d..9b6a3b9cb 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -7,6 +7,7 @@ use RABX;
use List::Util qw(min);
use Scalar::Util 'blessed';
use DateTime::Format::Pg;
+use Try::Tiny;
use strict;
use warnings;
@@ -87,8 +88,6 @@ sub example_places {
sub languages { [ 'de-ch,Deutsch,de_CH' ] }
sub language_override { 'de-ch' }
-sub default_link_zoom { 6 }
-
sub prettify_dt {
my $self = shift;
my $dt = shift;
@@ -236,10 +235,12 @@ my %public_holidays = map { $_ => 1 } (
'2019-01-01', '2019-01-02', '2019-04-19', '2019-04-22',
'2019-04-08', '2019-05-01', '2019-05-30', '2019-06-10',
'2019-08-01', '2019-09-09', '2019-12-25', '2019-12-26',
+ '2019-04-18', '2019-05-29', '2019-05-31', '2019-12-24', '2019-12-27', '2019-12-30', '2019-12-31',
'2020-01-01', '2020-01-02', '2020-04-10', '2020-04-13',
'2020-04-20', '2020-05-01', '2020-05-21', '2020-06-01',
'2020-09-14', '2020-12-25',
+ '2020-05-20', '2020-05-22', '2020-12-24', '2020-12-28', '2020-12-29', '2020-12-30', '2020-12-31',
'2021-01-01', '2021-04-02', '2021-04-05',
'2021-04-19', '2021-05-13', '2021-05-24',
@@ -315,6 +316,31 @@ sub get_or_check_overdue {
return $self->overdue($problem);
}
+sub report_page_data {
+ my $self = shift;
+ my $c = $self->{c};
+
+ $c->stash->{page} = 'reports';
+ $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};
+ FixMyStreet::Map::display_map(
+ $c,
+ latitude => @$pins ? $pins->[0]{latitude} : 0,
+ longitude => @$pins ? $pins->[0]{longitude} : 0,
+ area => 274456,
+ pins => $pins,
+ any_zoom => 1,
+ );
+ return 1;
+}
+
=head1 C<set_problem_state>
If the state has changed, sets the state and calls C::Admin's C<log_edit> action.
@@ -625,7 +651,7 @@ sub admin_report_edit {
&& $new_cat
&& $new_cat ne $problem->category
) {
- my $cat = $c->model('DB::Contact')->search({ category => $c->get_param('category') } )->first;
+ my $cat = $c->model('DB::Contact')->not_deleted->search({ category => $c->get_param('category') } )->first;
my $old_cat = $problem->category;
$problem->category( $new_cat );
$problem->external_body( undef );
@@ -1037,7 +1063,7 @@ sub munge_sendreport_params {
}
sub admin_fetch_all_bodies {
- my ( $self, @bodies ) = @_;
+ my ( $self ) = @_;
sub tree_sort {
my ( $level, $id, $sorted, $out ) = @_;
@@ -1047,26 +1073,30 @@ sub admin_fetch_all_bodies {
if ( $level == 0 ) {
@sorted = sort {
# Want Zurich itself at the top.
- return -1 if $sorted->{$a->id};
- return 1 if $sorted->{$b->id};
+ return -1 if $sorted->{$a->{id}};
+ return 1 if $sorted->{$b->{id}};
# Otherwise, by name
- strcoll($a->name, $b->name)
+ strcoll($a->{name}, $b->{name})
} @$array;
} else {
- @sorted = sort { strcoll($a->name, $b->name) } @$array;
+ @sorted = sort { strcoll($a->{name}, $b->{name}) } @$array;
}
foreach ( @sorted ) {
- $_->api_key( $level ); # Misuse
+ $_->{indent_level} = $level;
push @$out, $_;
- if ($sorted->{$_->id}) {
- tree_sort( $level+1, $_->id, $sorted, $out );
+ if ($sorted->{$_->{id}}) {
+ tree_sort( $level+1, $_->{id}, $sorted, $out );
}
}
}
+ my @bodies = FixMyStreet::DB->resultset('Body')->search(undef, {
+ columns => [ "id", "name", "deleted", "parent", "endpoint" ],
+ })->translated->with_children_count->all_sorted;
+
my %sorted;
foreach (@bodies) {
- my $p = $_->parent ? $_->parent->id : 0;
+ my $p = $_->{parent} ? $_->{parent}{id} : 0;
push @{$sorted{$p}}, $_;
}
@@ -1172,95 +1202,100 @@ sub admin_stats {
sub export_as_csv {
my ($self, $c, $params) = @_;
- $c->model('DB')->schema->storage->sql_maker->quote_char('"');
- my $csv = $c->stash->{csv} = {
- problems => $c->model('DB::Problem')->search_rs(
- $params,
- {
- join => ['admin_log_entries', 'user'],
- distinct => 1,
- columns => [
- 'id', 'created',
- 'latitude', 'longitude',
- 'cobrand', 'category',
- 'state', 'user_id',
- 'external_body',
- 'title', 'detail',
- 'photo',
- 'whensent', 'lastupdate',
- 'service',
- 'extra',
- { sum_time_spent => { sum => 'admin_log_entries.time_spent' } },
- 'name', 'user.id', 'user.email', 'user.phone', 'user.name',
- ]
- }
- ),
- headers => [
- 'Report ID', 'Created', 'Sent to Agency', 'Last Updated',
- 'E', 'N', 'Category', 'Status', 'Closure Status',
- 'UserID', 'User email', 'User phone', 'User name',
- 'External Body', 'Time Spent', 'Title', 'Detail',
- 'Media URL', 'Interface Used', 'Council Response',
- 'Strasse', 'Mast-Nr.', 'Haus-Nr.', 'Hydranten-Nr.',
- ],
- columns => [
- 'id', 'created', 'whensent',' lastupdate', 'local_coords_x',
- 'local_coords_y', 'category', 'state', 'closure_status',
- 'user_id', 'user_email', 'user_phone', 'user_name',
- 'body_name', 'sum_time_spent', 'title', 'detail',
- 'media_url', 'service', 'public_response',
- 'strasse', 'mast_nr',' haus_nr', 'hydranten_nr',
- ],
- extra_data => sub {
- my $report = shift;
-
- my $body_name = "";
- if ( my $external_body = $report->body($c) ) {
- $body_name = $external_body->name || '[Unknown body]';
- }
+ try {
+ $c->model('DB')->schema->storage->sql_maker->quote_char('"');
+ my $csv = $c->stash->{csv} = {
+ objects => $c->model('DB::Problem')->search_rs(
+ $params,
+ {
+ join => ['admin_log_entries', 'user'],
+ distinct => 1,
+ columns => [
+ 'id', 'created',
+ 'latitude', 'longitude',
+ 'cobrand', 'category',
+ 'state', 'user_id',
+ 'external_body',
+ 'title', 'detail',
+ 'photo',
+ 'whensent', 'lastupdate',
+ 'service',
+ 'extra',
+ { sum_time_spent => { sum => 'admin_log_entries.time_spent' } },
+ 'name', 'user.id', 'user.email', 'user.phone', 'user.name',
+ ]
+ }
+ ),
+ headers => [
+ 'Report ID', 'Created', 'Sent to Agency', 'Last Updated',
+ 'E', 'N', 'Category', 'Status', 'Closure Status',
+ 'UserID', 'User email', 'User phone', 'User name',
+ 'External Body', 'Time Spent', 'Title', 'Detail',
+ 'Media URL', 'Interface Used', 'Council Response',
+ 'Strasse', 'Mast-Nr.', 'Haus-Nr.', 'Hydranten-Nr.',
+ ],
+ columns => [
+ 'id', 'created', 'whensent',' lastupdate', 'local_coords_x',
+ 'local_coords_y', 'category', 'state', 'closure_status',
+ 'user_id', 'user_email', 'user_phone', 'user_name',
+ 'body_name', 'sum_time_spent', 'title', 'detail',
+ 'media_url', 'service', 'public_response',
+ 'strasse', 'mast_nr',' haus_nr', 'hydranten_nr',
+ ],
+ extra_data => sub {
+ my $report = shift;
+
+ my $body_name = "";
+ if ( my $external_body = $report->body($c) ) {
+ $body_name = $external_body->name || '[Unknown body]';
+ }
- my $detail = $report->detail;
- my $public_response = $report->get_extra_metadata('public_response') || '';
- my $metas = $report->get_extra_fields();
- my %extras;
- foreach my $field (@$metas) {
- $extras{$field->{name}} = $field->{value};
- }
+ my $detail = $report->detail;
+ my $public_response = $report->get_extra_metadata('public_response') || '';
+ my $metas = $report->get_extra_fields();
+ my %extras;
+ foreach my $field (@$metas) {
+ $extras{$field->{name}} = $field->{value};
+ }
- # replace newlines with HTML <br/> element
- $detail =~ s{\r?\n}{ <br/> }g;
- $public_response =~ s{\r?\n}{ <br/> }g if $public_response;
-
- # Assemble photo URL, if report has a photo
- my $photo_to_display = $c->cobrand->allow_photo_display($report);
- my $media_url = (@{$report->photos} && $photo_to_display)
- ? $c->cobrand->base_url . $report->photos->[$photo_to_display-1]->{url}
- : '';
-
- return {
- whensent => $report->whensent,
- lastupdate => $report->lastupdate,
- user_id => $report->user_id,
- user_email => $report->user->email || '',
- user_phone => $report->user->phone || '',
- user_name => $report->name,
- closure_status => $report->get_extra_metadata('closure_status') || '',
- body_name => $body_name,
- sum_time_spent => $report->get_column('sum_time_spent') || 0,
- detail => $detail,
- media_url => $media_url,
- service => $report->service || 'Web interface',
- public_response => $public_response,
- strasse => $extras{'strasse'} || '',
- mast_nr => $extras{'mast_nr'} || '',
- haus_nr => $extras{'haus_nr'} || '',
- hydranten_nr => $extras{'hydranten_nr'} || ''
- };
- },
- filename => 'stats',
+ # replace newlines with HTML <br/> element
+ $detail =~ s{\r?\n}{ <br/> }g;
+ $public_response =~ s{\r?\n}{ <br/> }g if $public_response;
+
+ # Assemble photo URL, if report has a photo
+ my $photo_to_display = $c->cobrand->allow_photo_display($report);
+ my $media_url = (@{$report->photos} && $photo_to_display)
+ ? $c->cobrand->base_url . $report->photos->[$photo_to_display-1]->{url}
+ : '';
+
+ return {
+ whensent => $report->whensent,
+ lastupdate => $report->lastupdate,
+ user_id => $report->user_id,
+ user_email => $report->user->email || '',
+ user_phone => $report->user->phone || '',
+ user_name => $report->name,
+ closure_status => $report->get_extra_metadata('closure_status') || '',
+ body_name => $body_name,
+ sum_time_spent => $report->get_column('sum_time_spent') || 0,
+ detail => $detail,
+ media_url => $media_url,
+ service => $report->service || 'Web interface',
+ public_response => $public_response,
+ strasse => $extras{'strasse'} || '',
+ mast_nr => $extras{'mast_nr'} || '',
+ haus_nr => $extras{'haus_nr'} || '',
+ hydranten_nr => $extras{'hydranten_nr'} || ''
+ };
+ },
+ filename => 'stats',
+ };
+ $c->forward('/dashboard/generate_csv');
+ } catch {
+ die $_;
+ } finally {
+ $c->model('DB')->schema->storage->sql_maker->quote_char('');
};
- $c->forward('/dashboard/generate_csv');
- $c->model('DB')->schema->storage->sql_maker->quote_char('');
}
sub problem_confirm_email_extras {
diff --git a/perllib/FixMyStreet/DB/Factories.pm b/perllib/FixMyStreet/DB/Factories.pm
index 56cff280b..5af9ed38f 100644
--- a/perllib/FixMyStreet/DB/Factories.pm
+++ b/perllib/FixMyStreet/DB/Factories.pm
@@ -94,9 +94,25 @@ sub data {
my $self = shift;
my %titles = (
+ 'Abandoned vehicles' => ['Car on pavement, has been there for months', 'Silver car outside house, never used'],
+ 'Bus stops' => ['Bus stop sign wonky', 'Information board broken'],
+ 'Dog fouling' => ['Bad dog fouling in alley way', 'Inconsiderate dog owner' ],
+ 'Flyposting' => ['Fence by road covered in posters', 'Under the bridge is a poster haven'],
+ 'Flytipping' => ['Flytipping on country lane', 'Ten bags of rubbish'],
+ 'Footpath/bridleway away from road' => ['Vehicle blocking footpath'],
+ 'Graffiti' => ['Graffiti', 'Graffiti', 'Offensive graffiti', 'Graffiti on the bridge', 'Remove graffiti'],
+ 'Parks/landscapes' => ['Full litter bins', 'Allotment gate needs repair'],
+ 'Pavements' => ['Hedge encroaching pavement', 'Many cracked slabs on street corner'],
'Potholes' => ['Deep pothole', 'Small pothole', 'Pothole in cycle lane', 'Pothole on busy pavement', 'Large pothole', 'Sinking manhole'],
+ 'Public toilets' => ['Door will not open'],
+ 'Roads/highways' => ['Restricted sight line by zig-zag lines', 'Missing lane markings'],
+ 'Road traffic signs' => ['Bent sign', 'Zebra crossing', 'Bollard missing'],
+ 'Rubbish (refuse and recycling)' => ['Missing bin', 'Bags left uncollected'],
+ 'Street cleaning' => ['Two abandoned trollies', 'Yet more litter'],
'Street lighting' => ['Faulty light', 'Street light not working', 'Lights out in tunnel', 'Light not coming on', 'Light not going off'],
- 'Graffiti' => ['Graffiti', 'Graffiti', 'Offensive graffiti', 'Graffiti on the bridge', 'Remove graffiti'],
+ 'Street nameplates' => ['Broken nameplate', 'Missing nameplate'],
+ 'Traffic lights' => ['Out of sync lights', 'Always on green', 'Broken light'],
+ 'Trees' => ['Young tree damaged', 'Tree looks dangerous in wind'],
'Other' => ['Loose drain cover', 'Flytipping on country lane', 'Vehicle blocking footpath', 'Hedge encroaching pavement', 'Full litter bins'],
);
my %photos = (
@@ -171,8 +187,8 @@ sub create_problem {
$params->{latitude} += rand(2 * $inaccurate_km) - $inaccurate_km;
$params->{longitude} += rand(3 * $inaccurate_km) - 1.5 * $inaccurate_km,
- $params->{title} = $titles->[$rand];
- $params->{detail} = $descs->[$rand];
+ $params->{title} ||= $titles->[$rand];
+ $params->{detail} ||= $descs->[$rand] || 'Please deal with this issue, thank you.';
$params->{photo_id} = $photo;
$params->{confirmed} = DateTime::Format::Pg->format_datetime($params->{confirmed});
return $self->create($params);
@@ -183,7 +199,7 @@ sub create_problem {
package FixMyStreet::DB::Factory::Body;
use parent -norequire, "FixMyStreet::DB::Factory::Base";
-use mySociety::MaPit;
+use FixMyStreet::MapIt;
__PACKAGE__->resultset(FixMyStreet::DB->resultset("Body"));
@@ -192,7 +208,7 @@ __PACKAGE__->exclude(['area_id', 'categories']);
__PACKAGE__->fields({
name => __PACKAGE__->callback(sub {
my $area_id = shift->get('area_id');
- my $area = mySociety::MaPit::call('area', $area_id);
+ my $area = FixMyStreet::MapIt::call('area', $area_id);
$area->{name};
}),
body_areas => __PACKAGE__->callback(sub {
@@ -212,7 +228,7 @@ sub key_field { 'id' }
package FixMyStreet::DB::Factory::Contact;
-use parent "DBIx::Class::Factory";
+use parent -norequire, "FixMyStreet::DB::Factory::Base";
__PACKAGE__->resultset(FixMyStreet::DB->resultset("Contact"));
@@ -224,8 +240,8 @@ __PACKAGE__->fields({
category => 'Other',
email => __PACKAGE__->callback(sub {
my $category = shift->get('category');
- (my $email = lc $_) =~ s/ /-/g;
- lc $category . '@example.org';
+ (my $email = lc $category) =~ s/ /-/g;
+ $email . '@example.org';
}),
state => 'confirmed',
editor => 'Factory',
@@ -233,6 +249,8 @@ __PACKAGE__->fields({
note => 'Created by factory',
});
+sub key_field { 'id' }
+
#######################
package FixMyStreet::DB::Factory::ResponseTemplate;
diff --git a/perllib/FixMyStreet/DB/RABXColumn.pm b/perllib/FixMyStreet/DB/RABXColumn.pm
index 5f1583018..d14b48dc8 100644
--- a/perllib/FixMyStreet/DB/RABXColumn.pm
+++ b/perllib/FixMyStreet/DB/RABXColumn.pm
@@ -59,7 +59,6 @@ sub rabx_column {
my $self = shift;
my $ser = shift;
return undef unless defined $ser;
- utf8::encode($ser) if utf8::is_utf8($ser);
my $h = new IO::String($ser);
return RABX::wire_rd($h);
},
@@ -78,18 +77,23 @@ sub rabx_column {
$RABX_COLUMNS{ _get_class_identifier($class) }{$col} = 1;
}
+# The underlying column should always be UTF-8 encoded bytes.
+sub get_column {
+ my ($self, $col) = @_;
+ my $res = $self->next::method ($col);
+ utf8::encode($res) if $RABX_COLUMNS{_get_class_identifier($self)}{$col} && utf8::is_utf8($res);
+ return $res;
+}
sub set_filtered_column {
my ($self, $col, $val) = @_;
- my $class = ref $self;
-
# because filtered objects may be expensive to marshall for storage there
# is a cache that attempts to detect if they have changed or not. For us
# this cache breaks things and our marshalling is cheap, so clear it when
# trying set a column.
delete $self->{_filtered_column}{$col}
- if $RABX_COLUMNS{ _get_class_identifier($class) }{$col};
+ if $RABX_COLUMNS{ _get_class_identifier($self) }{$col};
return $self->next::method($col, $val);
}
diff --git a/perllib/FixMyStreet/DB/Result/Abuse.pm b/perllib/FixMyStreet/DB/Result/Abuse.pm
index e8e554afa..7818eb743 100644
--- a/perllib/FixMyStreet/DB/Result/Abuse.pm
+++ b/perllib/FixMyStreet/DB/Result/Abuse.pm
@@ -8,14 +8,18 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("abuse");
__PACKAGE__->add_columns("email", { data_type => "text", is_nullable => 0 });
__PACKAGE__->set_primary_key("email");
-# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PnQhGMx+ktK++3gWOMJBpQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:6XdWpymMMUEC4WT9Yh0RLw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;
diff --git a/perllib/FixMyStreet/DB/Result/AdminLog.pm b/perllib/FixMyStreet/DB/Result/AdminLog.pm
index 1c9bd3a63..221690405 100644
--- a/perllib/FixMyStreet/DB/Result/AdminLog.pm
+++ b/perllib/FixMyStreet/DB/Result/AdminLog.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("admin_log");
__PACKAGE__->add_columns(
"id",
@@ -54,7 +58,7 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 14:38:36
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:y2xZ4BDv7H+f4vbIZyNflw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BLPP1KitphuY56ptaXhzgg
1;
diff --git a/perllib/FixMyStreet/DB/Result/Alert.pm b/perllib/FixMyStreet/DB/Result/Alert.pm
index 2a52a7bca..8979fa338 100644
--- a/perllib/FixMyStreet/DB/Result/Alert.pm
+++ b/perllib/FixMyStreet/DB/Result/Alert.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("alert");
__PACKAGE__->add_columns(
"id",
@@ -65,8 +69,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:5RNyB430T8PqtFlmGV/MUg
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:pWmsXAFvvjr4x1Q3Zsu4Cg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
@@ -75,17 +79,6 @@ use namespace::clean -except => [ 'meta' ];
with 'FixMyStreet::Roles::Abuser';
-my $stz = sub {
- my ( $orig, $self ) = ( shift, shift );
- my $s = $self->$orig(@_);
- return $s unless $s && UNIVERSAL::isa($s, "DateTime");
- FixMyStreet->set_time_zone($s);
- return $s;
-};
-
-around whensubscribed => $stz;
-around whendisabled => $stz;
-
=head2 confirm
$alert->confirm();
diff --git a/perllib/FixMyStreet/DB/Result/AlertSent.pm b/perllib/FixMyStreet/DB/Result/AlertSent.pm
index 83043a33b..d4e669f7f 100644
--- a/perllib/FixMyStreet/DB/Result/AlertSent.pm
+++ b/perllib/FixMyStreet/DB/Result/AlertSent.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("alert_sent");
__PACKAGE__->add_columns(
"alert_id",
@@ -31,8 +35,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/+Vodu8VJxJ0EY9P3Qjjjw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xriosaSCkOo/REOG1OxdQA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/AlertType.pm b/perllib/FixMyStreet/DB/Result/AlertType.pm
index 3aa9677e0..3d9603008 100644
--- a/perllib/FixMyStreet/DB/Result/AlertType.pm
+++ b/perllib/FixMyStreet/DB/Result/AlertType.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("alert_type");
__PACKAGE__->add_columns(
"ref",
@@ -47,8 +51,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:KDBYzNEAM5lPvZjb9cv22g
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:7JyCGS/rEvL1++p520749w
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm
index 74a38f225..663181746 100644
--- a/perllib/FixMyStreet/DB/Result/Body.pm
+++ b/perllib/FixMyStreet/DB/Result/Body.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("body");
__PACKAGE__->add_columns(
"id",
@@ -18,6 +22,12 @@ __PACKAGE__->add_columns(
is_nullable => 0,
sequence => "body_id_seq",
},
+ "name",
+ { data_type => "text", is_nullable => 0 },
+ "external_url",
+ { data_type => "text", is_nullable => 1 },
+ "parent",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"endpoint",
{ data_type => "text", is_nullable => 1 },
"jurisdiction",
@@ -36,20 +46,14 @@ __PACKAGE__->add_columns(
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
"send_extended_statuses",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
- "name",
- { data_type => "text", is_nullable => 0 },
- "parent",
- { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
- "deleted",
- { data_type => "boolean", default_value => \"false", is_nullable => 0 },
- "external_url",
- { data_type => "text", is_nullable => 1 },
"fetch_problems",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
"blank_updates_permitted",
- { data_type => "boolean", default_value => \"false", is_nullable => 1 },
+ { data_type => "boolean", default_value => \"false", is_nullable => 0 },
"convert_latlong",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "deleted",
+ { data_type => "boolean", default_value => \"false", is_nullable => 0 },
"extra",
{ data_type => "text", is_nullable => 1 },
);
@@ -126,22 +130,30 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-04-05 14:29:33
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HV8IM2C1ErrpvXoRTZ1B1Q
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8CuxbffDaYS7TFlgff1nEg
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
use Moo;
use namespace::clean;
+use FixMyStreet::MapIt;
with 'FixMyStreet::Roles::Translatable',
'FixMyStreet::Roles::Extra';
+sub _url {
+ my ( $obj, $cobrand, $args ) = @_;
+ my $uri = URI->new('/reports/' . $cobrand->short_name($obj));
+ $uri->query_form($args) if $args;
+ return $uri;
+}
+
sub url {
my ( $self, $c, $args ) = @_;
- # XXX $areas_info was used here for Norway parent - needs body parents, I guess
- return $c->uri_for( '/reports/' . $c->cobrand->short_name( $self ), $args || {} );
+ my $cobrand = $self->result_source->schema->cobrand;
+ return _url($self, $cobrand, $args);
}
__PACKAGE__->might_have(
@@ -174,7 +186,8 @@ sub first_area_children {
return unless $body_area;
my $cobrand = $self->result_source->schema->cobrand;
- my $children = mySociety::MaPit::call('area/children', $body_area->area_id,
+
+ my $children = FixMyStreet::MapIt::call('area/children', $body_area->area_id,
type => $cobrand->area_types_children,
);
@@ -197,7 +210,8 @@ sub get_cobrand_handler {
}
sub calculate_average {
- my ($self) = @_;
+ my ($self, $threshold) = @_;
+ $threshold ||= 0;
my $substmt = "select min(id) from comment where me.problem_id=comment.problem_id and (problem_state in ('fixed', 'fixed - council', 'fixed - user') or mark_fixed)";
my $subquery = FixMyStreet::DB->resultset('Comment')->to_body($self)->search({
@@ -207,6 +221,7 @@ sub calculate_average {
],
'me.id' => \"= ($substmt)",
'me.state' => 'confirmed',
+ 'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ],
}, {
select => [
{ extract => "epoch from me.confirmed-problem.confirmed", -as => 'time' },
@@ -217,12 +232,15 @@ sub calculate_average {
join => 'problem'
})->as_subselect_rs;
- my $avg = $subquery->search({
+ my $result = $subquery->search({
}, {
- select => [ { avg => "time" } ],
- as => [ qw/avg/ ],
- })->first->get_column('avg');
- return $avg;
+ select => [ { avg => "time" }, { count => "time" } ],
+ as => [ qw/avg count/ ],
+ })->first;
+ my $avg = $result->get_column('avg');
+ my $count = $result->get_column('count');
+
+ return $count >= $threshold ? $avg : undef;
}
1;
diff --git a/perllib/FixMyStreet/DB/Result/BodyArea.pm b/perllib/FixMyStreet/DB/Result/BodyArea.pm
index 4447777dc..7f0956c7d 100644
--- a/perllib/FixMyStreet/DB/Result/BodyArea.pm
+++ b/perllib/FixMyStreet/DB/Result/BodyArea.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("body_areas");
__PACKAGE__->add_columns(
"body_id",
@@ -25,8 +29,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+hzie6kHleUBoEt199c/nQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VPs3e9McGNO+Dd7C4pApxw
__PACKAGE__->set_primary_key(__PACKAGE__->columns);
diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm
index 8a4dbe475..5d0253ef4 100644
--- a/perllib/FixMyStreet/DB/Result/Comment.pm
+++ b/perllib/FixMyStreet/DB/Result/Comment.pm
@@ -6,10 +6,13 @@ package FixMyStreet::DB::Result::Comment;
use strict;
use warnings;
-use FixMyStreet::Template;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("comment");
__PACKAGE__->add_columns(
"id",
@@ -70,8 +73,8 @@ __PACKAGE__->add_columns(
{ data_type => "timestamp", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
-__PACKAGE__->might_have(
- "moderation_original_data",
+__PACKAGE__->has_many(
+ "moderation_original_datas",
"FixMyStreet::DB::Result::ModerationOriginalData",
{ "foreign.comment_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
@@ -90,8 +93,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZR+YNA1Jej3s+8mr52iq6Q
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CozqNY621I8G7kUPXi5RoQ
#
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
@@ -99,21 +102,32 @@ __PACKAGE__->rabx_column('extra');
use Moo;
use namespace::clean -except => [ 'meta' ];
+use FixMyStreet::Template;
with 'FixMyStreet::Roles::Abuser',
'FixMyStreet::Roles::Extra',
+ 'FixMyStreet::Roles::Moderation',
'FixMyStreet::Roles::PhotoSet';
-my $stz = sub {
- my ( $orig, $self ) = ( shift, shift );
- my $s = $self->$orig(@_);
- return $s unless $s && UNIVERSAL::isa($s, "DateTime");
- FixMyStreet->set_time_zone($s);
- return $s;
-};
+=head2 get_cobrand_logged
+
+Get a cobrand object for the cobrand the update was made on.
+
+e.g. if an update was logged at www.fixmystreet.com, this will be a
+FixMyStreet::Cobrand::FixMyStreet object.
+
+=cut
+
+has get_cobrand_logged => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $cobrand_class = FixMyStreet::Cobrand->get_class_for_moniker( $self->cobrand );
+ return $cobrand_class->new;
+ },
+);
-around created => $stz;
-around confirmed => $stz;
# You can replace this text with custom code or comments, and it will be preserved on regeneration
@@ -156,17 +170,6 @@ sub url {
return "/report/" . $self->problem_id . '#update_' . $self->id;
}
-=head2 latest_moderation_log_entry
-
-Return most recent ModerationLog object
-
-=cut
-
-sub latest_moderation_log_entry {
- my $self = shift;
- return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => { -desc => 'id' } })->first;
-}
-
__PACKAGE__->has_many(
"admin_log_entries",
"FixMyStreet::DB::Result::AdminLog",
@@ -177,24 +180,24 @@ __PACKAGE__->has_many(
}
);
-# we already had the `moderation_original_data` rel above, as inferred by
-# Schema::Loader, but that doesn't know about the problem_id mapping, so we now
-# (slightly hackishly) redefine here:
-#
-# we also add cascade_delete, though this seems to be insufficient.
-#
-# TODO: should add FK on moderation_original_data field for this, to get S::L to
-# pick up without hacks.
-
+# This will return the oldest moderation_original_data, if any.
+# The plural can be used to return all entries.
__PACKAGE__->might_have(
"moderation_original_data",
"FixMyStreet::DB::Result::ModerationOriginalData",
{ "foreign.comment_id" => "self.id",
"foreign.problem_id" => "self.problem_id",
},
- { cascade_copy => 0, cascade_delete => 1 },
+ { order_by => 'id',
+ rows => 1,
+ cascade_copy => 0, cascade_delete => 1 },
);
+sub moderation_filter {
+ my $self = shift;
+ { problem_id => $self->problem_id };
+}
+
=head2 meta_line
Returns a string to be used on a report update, describing some of the metadata
@@ -228,7 +231,9 @@ sub meta_line {
$body = "$body <img src='/cobrands/greenwich/favicon.png' alt=''>";
}
}
- my $can_view_contribute = $c->user_exists && $c->user->has_permission_to('view_body_contribute_details', $self->problem->bodies_str_ids);
+ my $cobrand_always_view_body_user = $c->cobrand->call_hook("always_view_body_contribute_details");
+ my $can_view_contribute = $cobrand_always_view_body_user ||
+ ($c->user_exists && $c->user->has_permission_to('view_body_contribute_details', $self->problem->bodies_str_ids));
if ($self->text) {
if ($can_view_contribute) {
$meta = sprintf( _( 'Posted by <strong>%s</strong> (%s) at %s' ), $body, $user_name, Utils::prettify_dt( $self->confirmed ) );
@@ -253,24 +258,22 @@ sub meta_line {
return $meta;
};
+sub problem_state_processed {
+ my $self = shift;
+ return 'fixed - user' if $self->mark_fixed;
+ return 'confirmed' if $self->mark_open;
+ return $self->problem_state;
+}
+
sub problem_state_display {
my ( $self, $c ) = @_;
- my $update_state = '';
- my $cobrand = $c->cobrand->moniker;
-
- if ($self->mark_fixed) {
- return FixMyStreet::DB->resultset("State")->display('fixed', 1);
- } elsif ($self->mark_open) {
- return FixMyStreet::DB->resultset("State")->display('confirmed', 1);
- } elsif ($self->problem_state) {
- my $state = $self->problem_state;
- my $cobrand_name = $cobrand;
- $cobrand_name = 'bromley' if $self->problem->to_body_named('Bromley');
- $update_state = FixMyStreet::DB->resultset("State")->display($state, 1, $cobrand_name);
- }
+ my $state = $self->problem_state_processed;
+ return '' unless $state;
- return $update_state;
+ my $cobrand_name = $c->cobrand->moniker;
+ $cobrand_name = 'bromley' if $self->problem->to_body_named('Bromley');
+ return FixMyStreet::DB->resultset("State")->display($state, 1, $cobrand_name);
}
sub is_latest {
@@ -298,4 +301,27 @@ sub hide {
return $ret;
}
+sub as_hashref {
+ my ($self, $c, $cols) = @_;
+
+ my $out = {
+ id => $self->id,
+ problem_id => $self->problem_id,
+ text => $self->text,
+ state => $self->state,
+ created => $self->created,
+ };
+
+ $out->{problem_state} = $self->problem_state_processed;
+
+ $out->{photos} = [ map { $_->{url} } @{$self->photos} ] if !$cols || $cols->{photos};
+
+ if ($self->confirmed) {
+ $out->{confirmed} = $self->confirmed if !$cols || $cols->{confirmed};
+ $out->{confirmed_pp} = $c->cobrand->prettify_dt( $self->confirmed ) if !$cols || $cols->{confirmed_pp};
+ }
+
+ return $out;
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm
index c544f084a..17620f279 100644
--- a/perllib/FixMyStreet/DB/Result/Contact.pm
+++ b/perllib/FixMyStreet/DB/Result/Contact.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("contacts");
__PACKAGE__->add_columns(
"id",
@@ -24,6 +28,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", default_value => "Other", is_nullable => 0 },
"email",
{ data_type => "text", is_nullable => 0 },
+ "state",
+ { data_type => "text", is_nullable => 0 },
"editor",
{ data_type => "text", is_nullable => 0 },
"whenedited",
@@ -42,8 +48,6 @@ __PACKAGE__->add_columns(
{ data_type => "text", default_value => "", is_nullable => 1 },
"send_method",
{ data_type => "text", is_nullable => 1 },
- "state",
- { data_type => "text", is_nullable => 0 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint("contacts_body_id_category_idx", ["body_id", "category"]);
@@ -73,8 +77,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-08 20:45:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t/VtPP11R8bbqPZdEVXffw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:f7XjQj4iABikbR4EZrjL3g
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
@@ -94,14 +98,9 @@ sub category_display {
$self->translate_column('category');
}
-sub get_metadata_for_input {
+sub get_metadata_for_editing {
my $self = shift;
- my $id_field = $self->id_field;
my @metadata = @{$self->get_extra_fields};
- # First, ones we always want to ignore (hard-coded, old system)
- @metadata = grep { $_->{code} !~ /^(easting|northing|closest_address|$id_field)$/ } @metadata;
- # Also ignore any we have with a 'server_set' automated attribute
- @metadata = grep { !$_->{automated} || $_->{automated} ne 'server_set' } @metadata;
# Just in case the extra data is in an old parsed format
foreach (@metadata) {
@@ -112,6 +111,16 @@ sub get_metadata_for_input {
return \@metadata;
}
+sub get_metadata_for_input {
+ my $self = shift;
+ my $metadata = $self->get_metadata_for_editing;
+
+ # Also ignore any we have with a 'server_set' automated attribute
+ my @metadata = grep { !$_->{automated} || $_->{automated} ne 'server_set' } @$metadata;
+
+ return \@metadata;
+}
+
sub id_field {
my $self = shift;
return $self->get_extra_metadata('id_field') || 'fixmystreet_id';
diff --git a/perllib/FixMyStreet/DB/Result/ContactDefectType.pm b/perllib/FixMyStreet/DB/Result/ContactDefectType.pm
index 2199f0b42..25d842e23 100644
--- a/perllib/FixMyStreet/DB/Result/ContactDefectType.pm
+++ b/perllib/FixMyStreet/DB/Result/ContactDefectType.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("contact_defect_types");
__PACKAGE__->add_columns(
"id",
@@ -38,8 +42,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VIczmM0OXXpWgQVpop3SMw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:yjQ/+17jn8fW8J70fFtvgg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/ContactResponsePriority.pm b/perllib/FixMyStreet/DB/Result/ContactResponsePriority.pm
index d5afd75a7..8406e2762 100644
--- a/perllib/FixMyStreet/DB/Result/ContactResponsePriority.pm
+++ b/perllib/FixMyStreet/DB/Result/ContactResponsePriority.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("contact_response_priorities");
__PACKAGE__->add_columns(
"id",
@@ -38,8 +42,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-09-06 15:33:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:kM/9jY1QSgakyPTvutS+hw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:NvXWYJu14GUXEHztl3Zp4w
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/ContactResponseTemplate.pm b/perllib/FixMyStreet/DB/Result/ContactResponseTemplate.pm
index 3c777533c..3139b2c84 100644
--- a/perllib/FixMyStreet/DB/Result/ContactResponseTemplate.pm
+++ b/perllib/FixMyStreet/DB/Result/ContactResponseTemplate.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("contact_response_templates");
__PACKAGE__->add_columns(
"id",
@@ -38,8 +42,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-08-24 11:29:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:d6niNsxi2AsijhvJSuQeKw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PE5+8AZp77pb+tDFEwiOqg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm
index c90bb9d66..5a6039d6a 100644
--- a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm
+++ b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("contacts_history");
__PACKAGE__->add_columns(
"contacts_history_id",
@@ -26,20 +30,20 @@ __PACKAGE__->add_columns(
{ data_type => "text", default_value => "Other", is_nullable => 0 },
"email",
{ data_type => "text", is_nullable => 0 },
+ "state",
+ { data_type => "text", is_nullable => 0 },
"editor",
{ data_type => "text", is_nullable => 0 },
"whenedited",
{ data_type => "timestamp", is_nullable => 0 },
"note",
{ data_type => "text", is_nullable => 0 },
- "state",
- { data_type => "text", is_nullable => 0 },
);
__PACKAGE__->set_primary_key("contacts_history_id");
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-08 20:45:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HTt0g29yXTM/WyHKN179FA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:es6F6L3MS8pEUDprFplnYg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/DefectType.pm b/perllib/FixMyStreet/DB/Result/DefectType.pm
index a2969f59e..baee066af 100644
--- a/perllib/FixMyStreet/DB/Result/DefectType.pm
+++ b/perllib/FixMyStreet/DB/Result/DefectType.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("defect_types");
__PACKAGE__->add_columns(
"id",
@@ -49,8 +53,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BBLjb/aAoTKJZerdYCeBMQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:d5Gkeysiz/1P/Ww4Xur0vA
__PACKAGE__->many_to_many( contacts => 'contact_defect_types', 'contact' );
diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
index d7240cd5d..18d2a7683 100644
--- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
+++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("moderation_original_data");
__PACKAGE__->add_columns(
"id",
@@ -37,9 +41,16 @@ __PACKAGE__->add_columns(
is_nullable => 0,
original => { default_value => \"now()" },
},
+ "extra",
+ { data_type => "text", is_nullable => 1 },
+ "category",
+ { data_type => "text", is_nullable => 1 },
+ "latitude",
+ { data_type => "double precision", is_nullable => 1 },
+ "longitude",
+ { data_type => "double precision", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
-__PACKAGE__->add_unique_constraint("moderation_original_data_comment_id_key", ["comment_id"]);
__PACKAGE__->belongs_to(
"comment",
"FixMyStreet::DB::Result::Comment",
@@ -59,9 +70,149 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:DBtGjCJykDtLnGtkj638eA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FLKiZELcfBcc9VwHU2MZYQ
+
+use Moo;
+use Text::Diff;
+use Data::Dumper;
+
+with 'FixMyStreet::Roles::Extra';
+
+__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
+__PACKAGE__->rabx_column('extra');
+
+sub admin_log {
+ my $self = shift;
+ my $rs = $self->result_source->schema->resultset("AdminLog");
+ my $log = $rs->search({
+ object_id => $self->id,
+ object_type => 'moderation',
+ })->first;
+ return $log;
+}
+
+sub compare_with {
+ my ($self, $other) = @_;
+ if ($self->comment_id) {
+ my $new_detail = $other->can('text') ? $other->text : $other->detail;
+ return {
+ detail => string_diff($self->detail, $new_detail),
+ photo => $self->compare_photo($other),
+ anonymous => $self->compare_anonymous($other),
+ extra => $self->compare_extra($other),
+ };
+ }
+ return {
+ title => string_diff($self->title, $other->title),
+ detail => string_diff($self->detail, $other->detail),
+ photo => $self->compare_photo($other),
+ anonymous => $self->compare_anonymous($other),
+ coords => $self->compare_coords($other),
+ category => string_diff($self->category, $other->category, single => 1),
+ extra => $self->compare_extra($other),
+ }
+}
+
+sub compare_anonymous {
+ my ($self, $other) = @_;
+ string_diff(
+ $self->anonymous ? _('Yes') : _('No'),
+ $other->anonymous ? _('Yes') : _('No'),
+ );
+}
+
+sub compare_coords {
+ my ($self, $other) = @_;
+ return '' unless $self->latitude && $self->longitude;
+ my $old = join ',', $self->latitude, $self->longitude;
+ my $new = join ',', $other->latitude, $other->longitude;
+ string_diff($old, $new, single => 1);
+}
+
+sub compare_photo {
+ my ($self, $other) = @_;
+
+ my $old = $self->photo || '';
+ my $new = $other->photo || '';
+ return '' if $old eq $new;
+
+ $old = [ split /,/, $old ];
+ $new = [ split /,/, $new ];
+
+ my $diff = Algorithm::Diff->new( $old, $new );
+ my (@added, @deleted);
+ while ( $diff->Next ) {
+ next if $diff->Same;
+ push @deleted, $diff->Items(1);
+ push @added, $diff->Items(2);
+ }
+ return (join ', ', map {
+ "<del style='background-color:#fcc'>$_</del>";
+ } @deleted) . (join ', ', map {
+ "<ins style='background-color:#cfc'>$_</ins>";
+ } @added);
+}
+
+sub compare_extra {
+ my ($self, $other) = @_;
+
+ my $old = $self->get_extra_metadata;
+ my $new = $other->get_extra_metadata;
+
+ my $both = { %$old, %$new };
+ my @all_keys = sort keys %$both;
+ my @s;
+ foreach (@all_keys) {
+ if ($old->{$_} && $new->{$_}) {
+ push @s, string_diff("$_ = $old->{$_}", "$_ = $new->{$_}");
+ } elsif ($new->{$_}) {
+ push @s, string_diff("", "$_ = $new->{$_}");
+ } else {
+ push @s, string_diff("$_ = $old->{$_}", "");
+ }
+ }
+ return join ', ', grep { $_ } @s;
+}
+
+sub extra_diff {
+ my ($self, $other, $key) = @_;
+ my $o = $self->get_extra_metadata($key);
+ my $n = $other->get_extra_metadata($key);
+ return string_diff($o, $n);
+}
+
+sub string_diff {
+ my ($old, $new, %options) = @_;
+
+ return '' if $old eq $new;
+
+ $old = FixMyStreet::Template::html_filter($old);
+ $new = FixMyStreet::Template::html_filter($new);
+ if ($options{single}) {
+ return unless $old;
+ $old = [ $old ];
+ $new = [ $new ];
+ }
+ $old = [ split //, $old ] unless ref $old;
+ $new = [ split //, $new ] unless ref $new;
+ my $diff = Algorithm::Diff->new( $old, $new );
+ my $string;
+ while ($diff->Next) {
+ my $d = $diff->Diff;
+ if ($d & 1) {
+ my $deleted = join '', $diff->Items(1);
+ $string .= "<del style='background-color:#fcc'>$deleted</del>";
+ }
+ my $inserted = join '', $diff->Items(2);
+ if ($d & 2) {
+ $string .= "<ins style='background-color:#cfc'>$inserted</ins>";
+ } else {
+ $string .= $inserted;
+ }
+ }
+ return $string;
+}
-# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index f67e0b0f8..dc45091ee 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("problem");
__PACKAGE__->add_columns(
"id",
@@ -20,14 +24,38 @@ __PACKAGE__->add_columns(
},
"postcode",
{ data_type => "text", is_nullable => 0 },
+ "latitude",
+ { data_type => "double precision", is_nullable => 0 },
+ "longitude",
+ { data_type => "double precision", is_nullable => 0 },
+ "bodies_str",
+ { data_type => "text", is_nullable => 1 },
+ "bodies_missing",
+ { data_type => "text", is_nullable => 1 },
+ "areas",
+ { data_type => "text", is_nullable => 0 },
+ "category",
+ { data_type => "text", default_value => "Other", is_nullable => 0 },
"title",
{ data_type => "text", is_nullable => 0 },
"detail",
{ data_type => "text", is_nullable => 0 },
"photo",
{ data_type => "bytea", is_nullable => 1 },
+ "used_map",
+ { data_type => "boolean", is_nullable => 0 },
+ "user_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"name",
{ data_type => "text", is_nullable => 0 },
+ "anonymous",
+ { data_type => "boolean", is_nullable => 0 },
+ "external_id",
+ { data_type => "text", is_nullable => 1 },
+ "external_body",
+ { data_type => "text", is_nullable => 1 },
+ "external_team",
+ { data_type => "text", is_nullable => 1 },
"created",
{
data_type => "timestamp",
@@ -35,57 +63,37 @@ __PACKAGE__->add_columns(
is_nullable => 0,
original => { default_value => \"now()" },
},
- "state",
- { data_type => "text", is_nullable => 0 },
- "whensent",
- { data_type => "timestamp", is_nullable => 1 },
- "used_map",
- { data_type => "boolean", is_nullable => 0 },
- "bodies_str",
- { data_type => "text", is_nullable => 1 },
- "anonymous",
- { data_type => "boolean", is_nullable => 0 },
- "category",
- { data_type => "text", default_value => "Other", is_nullable => 0 },
"confirmed",
{ data_type => "timestamp", is_nullable => 1 },
- "send_questionnaire",
- { data_type => "boolean", default_value => \"true", is_nullable => 0 },
- "lastupdate",
- {
- data_type => "timestamp",
- default_value => \"current_timestamp",
- is_nullable => 0,
- original => { default_value => \"now()" },
- },
- "areas",
+ "state",
{ data_type => "text", is_nullable => 0 },
- "service",
- { data_type => "text", default_value => "", is_nullable => 0 },
"lang",
{ data_type => "text", default_value => "en-gb", is_nullable => 0 },
+ "service",
+ { data_type => "text", default_value => "", is_nullable => 0 },
"cobrand",
{ data_type => "text", default_value => "", is_nullable => 0 },
"cobrand_data",
{ data_type => "text", default_value => "", is_nullable => 0 },
- "latitude",
- { data_type => "double precision", is_nullable => 0 },
- "longitude",
- { data_type => "double precision", is_nullable => 0 },
- "external_id",
- { data_type => "text", is_nullable => 1 },
- "external_body",
- { data_type => "text", is_nullable => 1 },
- "external_team",
+ "lastupdate",
+ {
+ data_type => "timestamp",
+ default_value => \"current_timestamp",
+ is_nullable => 0,
+ original => { default_value => \"now()" },
+ },
+ "whensent",
+ { data_type => "timestamp", is_nullable => 1 },
+ "send_questionnaire",
+ { data_type => "boolean", default_value => \"true", is_nullable => 0 },
+ "extra",
{ data_type => "text", is_nullable => 1 },
- "user_id",
- { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"flagged",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
- "extra",
- { data_type => "text", is_nullable => 1 },
"geocode",
{ data_type => "bytea", is_nullable => 1 },
+ "response_priority_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"send_fail_count",
{ data_type => "integer", default_value => 0, is_nullable => 0 },
"send_fail_reason",
@@ -104,10 +112,6 @@ __PACKAGE__->add_columns(
{ data_type => "integer", default_value => 0, is_nullable => 1 },
"subcategory",
{ data_type => "text", is_nullable => 1 },
- "bodies_missing",
- { data_type => "text", is_nullable => 1 },
- "response_priority_id",
- { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"defect_type_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
);
@@ -166,8 +170,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8zzWlJX7OQOdvrGxKuZUmg
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hUXle+TtlkDkxkBrVa/u+g
# Add fake relationship to stored procedure table
__PACKAGE__->has_one(
@@ -177,11 +181,15 @@ __PACKAGE__->has_one(
{ cascade_copy => 0, cascade_delete => 0 },
);
+# This will return the oldest moderation_original_data, if any.
+# The plural can be used to return all entries.
__PACKAGE__->might_have(
"moderation_original_data",
"FixMyStreet::DB::Result::ModerationOriginalData",
{ "foreign.problem_id" => "self.id" },
{ where => { 'comment_id' => undef },
+ order_by => 'id',
+ rows => 1,
cascade_copy => 0, cascade_delete => 1 },
);
@@ -206,6 +214,7 @@ my $IM = eval {
with 'FixMyStreet::Roles::Abuser',
'FixMyStreet::Roles::Extra',
+ 'FixMyStreet::Roles::Moderation',
'FixMyStreet::Roles::Translatable',
'FixMyStreet::Roles::PhotoSet';
@@ -318,19 +327,6 @@ sub visible_states_remove {
}
}
-my $stz = sub {
- my ( $orig, $self ) = ( shift, shift );
- my $s = $self->$orig(@_);
- return $s unless $s && UNIVERSAL::isa($s, "DateTime");
- FixMyStreet->set_time_zone($s);
- return $s;
-};
-
-around created => $stz;
-around confirmed => $stz;
-around whensent => $stz;
-around lastupdate => $stz;
-
around service => sub {
my ( $orig, $self ) = ( shift, shift );
# service might be undef if e.g. unsaved code report
@@ -663,7 +659,7 @@ sub body {
my @body_names = sort map {
my $name = $_->name;
if ($c and FixMyStreet->config('AREA_LINKS_FROM_PROBLEMS')) {
- '<a href="' . $_->url($c) . '">' . $name . '</a>';
+ '<a href="' . $_->url . '">' . $name . '</a>';
} else {
$name;
}
@@ -759,7 +755,7 @@ sub defect_types {
# Note: this only makes sense when called on a problem that has been sent!
sub can_display_external_id {
my $self = shift;
- if ($self->external_id && $self->send_method_used && $self->to_body_named('Oxfordshire|Angus')) {
+ if ($self->external_id && $self->send_method_used && $self->to_body_named('Oxfordshire|Lincolnshire')) {
return 1;
}
return 0;
@@ -780,7 +776,7 @@ sub duration_string {
sub local_coords {
my $self = shift;
- my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($self->cobrand)->new;
+ my $cobrand = $self->get_cobrand_logged;
if ($cobrand->moniker eq 'zurich') {
my ($x, $y) = Geo::Coordinates::CH1903Plus::from_latlon($self->latitude, $self->longitude);
return ( int($x+0.5), int($y+0.5) );
@@ -893,11 +889,12 @@ bodies by some mechanism. Right now that mechanism is Open311.
sub updates_sent_to_body {
my $self = shift;
- return unless $self->send_method_used && $self->send_method_used eq 'Open311';
+ return unless $self->send_method_used && $self->send_method_used =~ /Open311/;
# Some bodies only send updates *to* FMS, they don't receive updates.
- # NB See also the list in bin/send-comments
- my $excluded = qr{Lewisham|Oxfordshire};
+ my $cobrand = $self->get_cobrand_logged;
+ my $handler = $cobrand->call_hook(get_body_handler_for_problem => $self);
+ return 0 if $handler && $handler->call_hook('open311_post_update_skip');
my @bodies = values %{ $self->bodies };
my @updates_sent = grep {
@@ -905,8 +902,7 @@ sub updates_sent_to_body {
(
$_->send_method eq 'Open311' ||
$_->send_method eq 'Noop' # Sending might be temporarily disabled
- ) &&
- !($_->name =~ /$excluded/)
+ )
} @bodies;
return scalar @updates_sent;
}
@@ -922,6 +918,12 @@ sub add_send_method {
}
}
+sub resend {
+ my $self = shift;
+ $self->whensent(undef);
+ $self->send_method_used(undef);
+}
+
sub as_hashref {
my ($self, $c, $cols) = @_;
@@ -952,17 +954,6 @@ sub as_hashref {
return $out;
}
-=head2 latest_moderation_log_entry
-
-Return most recent ModerationLog object
-
-=cut
-
-sub latest_moderation_log_entry {
- my $self = shift;
- return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => { -desc => 'id' } })->first;
-}
-
__PACKAGE__->has_many(
"admin_log_entries",
"FixMyStreet::DB::Result::AdminLog",
@@ -973,6 +964,11 @@ __PACKAGE__->has_many(
}
);
+sub moderation_filter {
+ my $self = shift;
+ { comment_id => undef };
+}
+
sub get_time_spent {
my $self = shift;
my $admin_logs = $self->admin_log_entries->search({},
@@ -1017,6 +1013,7 @@ sub pin_data {
id => $self->id,
title => $title,
problem => $self,
+ draggable => $opts{draggable},
type => $opts{type},
}
};
diff --git a/perllib/FixMyStreet/DB/Result/Questionnaire.pm b/perllib/FixMyStreet/DB/Result/Questionnaire.pm
index 30f2ab7ce..2d5445669 100644
--- a/perllib/FixMyStreet/DB/Result/Questionnaire.pm
+++ b/perllib/FixMyStreet/DB/Result/Questionnaire.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("questionnaire");
__PACKAGE__->add_columns(
"id",
@@ -40,21 +44,17 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:oL1Hk4/bNG14CY74GA75SA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:AWRb6itjsVkG5VUDRmBTIg
use Moo;
use namespace::clean -except => [ 'meta' ];
-my $stz = sub {
- my ( $orig, $self ) = ( shift, shift );
- my $s = $self->$orig(@_);
- return $s unless $s && UNIVERSAL::isa($s, "DateTime");
- FixMyStreet->set_time_zone($s);
- return $s;
-};
-
-around whensent => $stz;
-around whenanswered => $stz;
+sub marks_fixed {
+ my $self = shift;
+ my $new_fixed = FixMyStreet::DB::Result::Problem->fixed_states()->{$self->new_state};
+ my $old_fixed = FixMyStreet::DB::Result::Problem->fixed_states()->{$self->old_state};
+ return $new_fixed && !$old_fixed;
+}
1;
diff --git a/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm b/perllib/FixMyStreet/DB/Result/ReportExtraField.pm
index 27a6bd2c6..f88169bba 100644
--- a/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm
+++ b/perllib/FixMyStreet/DB/Result/ReportExtraField.pm
@@ -1,5 +1,5 @@
use utf8;
-package FixMyStreet::DB::Result::ReportExtraFields;
+package FixMyStreet::DB::Result::ReportExtraField;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("report_extra_fields");
__PACKAGE__->add_columns(
"id",
@@ -30,8 +34,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("id");
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-28 09:51:34
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:LkfbsUInnEyXowdcCEPjUQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 15:41:27
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:yRF676ybdkfalMwZ9V+yhw
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
diff --git a/perllib/FixMyStreet/DB/Result/ResponsePriority.pm b/perllib/FixMyStreet/DB/Result/ResponsePriority.pm
index df54cfa08..a478ac7b9 100644
--- a/perllib/FixMyStreet/DB/Result/ResponsePriority.pm
+++ b/perllib/FixMyStreet/DB/Result/ResponsePriority.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("response_priorities");
__PACKAGE__->add_columns(
"id",
@@ -20,10 +24,10 @@ __PACKAGE__->add_columns(
},
"body_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
- "name",
- { data_type => "text", is_nullable => 0 },
"deleted",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "name",
+ { data_type => "text", is_nullable => 0 },
"description",
{ data_type => "text", is_nullable => 1 },
"external_id",
@@ -53,8 +57,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-09-12 09:32:53
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JBIHFnaLvXCAUjgwTSB3CQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gIttzSJcQ8GxTowrQZ8oAw
__PACKAGE__->many_to_many( contacts => 'contact_response_priorities', 'contact' );
diff --git a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
index 73e0d898e..85bf80aef 100644
--- a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
+++ b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("response_templates");
__PACKAGE__->add_columns(
"id",
@@ -54,8 +58,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-03-22 11:18:36
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:p0+/jFma6H9eZ3MZAJQRaQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MzTa7p2rryKkxbRi7zN+Uw
__PACKAGE__->many_to_many( contacts => 'contact_response_templates', 'contact' );
diff --git a/perllib/FixMyStreet/DB/Result/Secret.pm b/perllib/FixMyStreet/DB/Result/Secret.pm
index 449dfec0e..045375fef 100644
--- a/perllib/FixMyStreet/DB/Result/Secret.pm
+++ b/perllib/FixMyStreet/DB/Result/Secret.pm
@@ -8,13 +8,17 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("secret");
__PACKAGE__->add_columns("secret", { data_type => "text", is_nullable => 0 });
-# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9XiWSKJ1PD3LSYjrSA3drw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:mVU/XGxS3DVhEcHTA2srgA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/Session.pm b/perllib/FixMyStreet/DB/Result/Session.pm
index a478c5444..94f7e823c 100644
--- a/perllib/FixMyStreet/DB/Result/Session.pm
+++ b/perllib/FixMyStreet/DB/Result/Session.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("sessions");
__PACKAGE__->add_columns(
"id",
@@ -21,8 +25,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("id");
-# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MVmCn4gLQWXTDIIaDHiVmA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HoYrCwULpxJVJ1m9ASMk3A
use Storable;
use MIME::Base64;
diff --git a/perllib/FixMyStreet/DB/Result/State.pm b/perllib/FixMyStreet/DB/Result/State.pm
index b8a35d42b..66477111b 100644
--- a/perllib/FixMyStreet/DB/Result/State.pm
+++ b/perllib/FixMyStreet/DB/Result/State.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("state");
__PACKAGE__->add_columns(
"id",
@@ -30,8 +34,8 @@ __PACKAGE__->add_unique_constraint("state_label_key", ["label"]);
__PACKAGE__->add_unique_constraint("state_name_key", ["name"]);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-08-22 15:17:43
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dvtAOpeYqEF9T3otHHgLqw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:f/QeR3FYL/4wIGRu3c/C/A
use Moo;
use namespace::clean;
diff --git a/perllib/FixMyStreet/DB/Result/Token.pm b/perllib/FixMyStreet/DB/Result/Token.pm
index a60e23839..444d5e5a8 100644
--- a/perllib/FixMyStreet/DB/Result/Token.pm
+++ b/perllib/FixMyStreet/DB/Result/Token.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("token");
__PACKAGE__->add_columns(
"scope",
@@ -28,8 +32,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("scope", "token");
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HkvzOY5STjOdXN64hxg5NA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:km/1K3PurX8bbgnYPWgLIA
use mySociety::AuthToken;
diff --git a/perllib/FixMyStreet/DB/Result/Translation.pm b/perllib/FixMyStreet/DB/Result/Translation.pm
index fafc7ccf1..4d6373d40 100644
--- a/perllib/FixMyStreet/DB/Result/Translation.pm
+++ b/perllib/FixMyStreet/DB/Result/Translation.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("translation");
__PACKAGE__->add_columns(
"id",
@@ -36,8 +40,8 @@ __PACKAGE__->add_unique_constraint(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-14 23:24:32
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:///VNqg4BOuO29xKhnY8vw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EsseG51ZpQa5QYHPCpkL8A
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm
index 8b539f85d..d01ba92d0 100644
--- a/perllib/FixMyStreet/DB/Result/User.pm
+++ b/perllib/FixMyStreet/DB/Result/User.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("users");
__PACKAGE__->add_columns(
"id",
@@ -36,16 +40,6 @@ __PACKAGE__->add_columns(
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
"is_superuser",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
- "title",
- { data_type => "text", is_nullable => 1 },
- "twitter_id",
- { data_type => "bigint", is_nullable => 1 },
- "facebook_id",
- { data_type => "bigint", is_nullable => 1 },
- "area_id",
- { data_type => "integer", is_nullable => 1 },
- "extra",
- { data_type => "text", is_nullable => 1 },
"created",
{
data_type => "timestamp",
@@ -60,6 +54,16 @@ __PACKAGE__->add_columns(
is_nullable => 0,
original => { default_value => \"now()" },
},
+ "title",
+ { data_type => "text", is_nullable => 1 },
+ "twitter_id",
+ { data_type => "bigint", is_nullable => 1 },
+ "facebook_id",
+ { data_type => "bigint", is_nullable => 1 },
+ "extra",
+ { data_type => "text", is_nullable => 1 },
+ "area_ids",
+ { data_type => "integer[]", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint("users_facebook_id_key", ["facebook_id"]);
@@ -119,8 +123,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-05-23 18:54:36
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/V7+Ygv/t6VX8dDhNGN16w
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BCCqv3JCec8psuRk/SdCJQ
# These are not fully unique constraints (they only are when the *_verified
# is true), but this is managed in ResultSet::User's find() wrapper.
@@ -131,6 +135,7 @@ __PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
use Moo;
+use Text::CSV;
use FixMyStreet::SMS;
use mySociety::EmailUtil;
use namespace::clean -except => [ 'meta' ];
@@ -175,8 +180,8 @@ sub phone_display {
sub latest_anonymity {
my $self = shift;
- my $p = $self->problems->search(undef, { order_by => { -desc => 'id' } } )->first;
- my $c = $self->comments->search(undef, { order_by => { -desc => 'id' } } )->first;
+ my $p = $self->problems->search(undef, { rows => 1, order_by => { -desc => 'id' } } )->first;
+ my $c = $self->comments->search(undef, { rows => 1, order_by => { -desc => 'id' } } )->first;
my $p_created = $p ? $p->created->epoch : 0;
my $c_created = $c ? $c->created->epoch : 0;
my $obj = $p_created >= $c_created ? $p : $c;
@@ -291,6 +296,11 @@ sub body {
return $self->from_body->name;
}
+sub moderating_user_name {
+ my $self = shift;
+ return $self->body || _('an administrator');
+}
+
=head2 belongs_to_body
$belongs_to_body = $user->belongs_to_body( $bodies );
@@ -329,6 +339,37 @@ sub split_name {
return { first => $first || '', last => $last || '' };
}
+sub can_moderate {
+ my ($self, $object, $perms) = @_;
+
+ my ($type, $ids);
+ if ($object->isa("FixMyStreet::DB::Result::Comment")) {
+ $type = 'update';
+ $ids = $object->problem->bodies_str_ids;
+ } else {
+ $type = 'problem';
+ $ids = $object->bodies_str_ids;
+ }
+
+ my $staff_perm = exists($perms->{staff}) ? $perms->{staff} : $self->has_permission_to(moderate => $ids);
+ return 1 if $staff_perm;
+
+ # See if the cobrand wants to allow it in some circumstance
+ my $cobrand = $self->result_source->schema->cobrand;
+ return $cobrand->call_hook('moderate_permission', $self, $type => $object);
+}
+
+sub can_moderate_title {
+ my ($self, $problem, $perm) = @_;
+
+ # Must have main permission, this is to potentially restrict only
+ return 0 unless $perm;
+
+ # If hook returns anything use it, otherwise default to yes
+ my $cobrand = $self->result_source->schema->cobrand;
+ return $cobrand->call_hook('moderate_permission_title', $self, $problem) // 1;
+}
+
has body_permissions => (
is => 'ro',
lazy => 1,
@@ -339,13 +380,16 @@ has body_permissions => (
);
sub permissions {
- my ($self, $c, $body_id) = @_;
+ my ($self, $problem) = @_;
+ my $cobrand = $self->result_source->schema->cobrand;
if ($self->is_superuser) {
- my $perms = $c->cobrand->available_permissions;
+ my $perms = $cobrand->available_permissions;
return { map { %$_ } values %$perms };
}
+ my $body_id = $problem->bodies_str;
+
return unless $self->belongs_to_body($body_id);
my @permissions = grep { $_->body_id == $self->from_body->id } @{$self->body_permissions};
@@ -544,6 +588,17 @@ has categories => (
},
);
+has categories_string => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $csv = Text::CSV->new;
+ $csv->combine(@{$self->categories});
+ return $csv->string;
+ },
+);
+
sub set_last_active {
my $self = shift;
my $time = shift;
@@ -551,4 +606,19 @@ sub set_last_active {
$self->last_active($time or \'current_timestamp');
}
+has areas_hash => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my %ids = map { $_ => 1 } @{$self->area_ids || []};
+ return \%ids;
+ },
+);
+
+sub in_area {
+ my ($self, $area) = @_;
+ return $self->areas_hash->{$area};
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/UserBodyPermission.pm b/perllib/FixMyStreet/DB/Result/UserBodyPermission.pm
index a118a1996..8fdabbdda 100644
--- a/perllib/FixMyStreet/DB/Result/UserBodyPermission.pm
+++ b/perllib/FixMyStreet/DB/Result/UserBodyPermission.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("user_body_permissions");
__PACKAGE__->add_columns(
"id",
@@ -44,8 +48,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2014-06-05 15:46:02
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:IWy2rYBU7WP6MyIkLYsc9Q
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:mcgnPaCmEuLWdzB3GuQiTg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm b/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm
index 1e893c7a9..cd1716f02 100644
--- a/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm
+++ b/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm
@@ -8,7 +8,11 @@ use strict;
use warnings;
use base 'DBIx::Class::Core';
-__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->load_components(
+ "FilterColumn",
+ "FixMyStreet::InflateColumn::DateTime",
+ "FixMyStreet::EncodedColumn",
+);
__PACKAGE__->table("user_planned_reports");
__PACKAGE__->add_columns(
"id",
@@ -47,8 +51,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 15:03:08
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:mv7koDhvZSBW/4aQivtpAQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A9ICDFNVzkmd/erdtYdeVA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/ResultSet/Body.pm b/perllib/FixMyStreet/DB/ResultSet/Body.pm
index 0aa3e8240..4e9661d2e 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Body.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Body.pm
@@ -41,7 +41,7 @@ This restricts the ResultSet to bodies that are not marked as deleted.
sub active {
my $rs = shift;
- $rs->search({ deleted => 0 });
+ $rs->search({ 'me.deleted' => 0 });
}
=item translated
@@ -61,6 +61,22 @@ sub translated {
});
}
+=item with_parent_name
+
+This adds the parent name associated with each body to the ResultSet,
+in the parent_name column.
+
+=cut
+
+sub with_parent_name {
+ my $rs = shift;
+ $rs->search(undef, {
+ '+select' => [ 'parent.name' ],
+ '+as' => [ 'parent_name' ],
+ join => 'parent',
+ });
+}
+
=item with_area_count
This adds the number of areas associated with each body to the ResultSet,
@@ -78,10 +94,45 @@ sub with_area_count {
});
}
+=item with_defect_type_count
+
+This adds the number of defect types associated with each body to the
+ResultSet, in the defect_type_count column.
+
+=cut
+
+sub with_defect_type_count {
+ my $rs = shift;
+ $rs->search(undef, {
+ '+select' => [ { count => 'defect_types.name' } ],
+ '+as' => [ 'defect_type_count' ],
+ join => 'defect_types',
+ distinct => 1,
+ });
+}
+
+=item with_children_count
+
+This adds the number of children associated with each body to the
+ResultSet, in the children_count column.
+
+=cut
+
+sub with_children_count {
+ my $rs = shift;
+ $rs->search(undef, {
+ '+select' => [ { count => 'bodies.id' } ],
+ '+as' => [ 'children_count' ],
+ join => 'bodies',
+ distinct => 1,
+ });
+}
+
=item all_sorted
-This returns all results, as C<all()>, but sorted by their name column
-(which will be the translated names if present).
+This returns all results, as C<all()>, but sorted by their name (including
+the translated names, if present), and as simple hashrefs not objects, for
+performance reasons.
=back
@@ -89,8 +140,34 @@ This returns all results, as C<all()>, but sorted by their name column
sub all_sorted {
my $rs = shift;
- my @bodies = $rs->all;
- @bodies = sort { strcoll($a->name, $b->name) } @bodies;
+
+ # Use a HashRefInflator here to return simple hashrefs rather than full
+ # objects. This is quicker if you have a large number of bodies; note
+ # fetching only the columns you need provides even more of a speed up.
+ my @bodies = $rs->search(undef, {
+ result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+ })->all;
+ @bodies = sort { strcoll($a->{msgstr} || $a->{name}, $b->{msgstr} || $b->{name}) } @bodies;
+
+ my $cobrand = $rs->result_source->schema->cobrand;
+
+ foreach my $body (@bodies) {
+ $body->{parent} = { id => $body->{parent}, name => $body->{parent_name} } if $body->{parent};
+
+ # DEPRECATED: url(c, query_params) -> url
+ $body->{url} = sub {
+ my ($c, $args) = @_;
+ return FixMyStreet::DB::Result::Body::_url($body, $cobrand, $args);
+ };
+
+ # DEPRECATED: get_column('area_count') -> area_count
+ next unless defined $body->{area_count};
+ $body->{get_column} = sub {
+ my $key = shift;
+ return $body->{$key};
+ };
+ }
+
return @bodies;
}
diff --git a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm
index b075e3664..2ebe309e3 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm
@@ -10,31 +10,34 @@ sub to_body {
}
sub nearby {
- my ( $rs, $c, $dist, $ids, $limit, $mid_lat, $mid_lon, $categories, $states, $extra_params ) = @_;
+ my ( $rs, $c, %args ) = @_;
- unless ( $states ) {
- $states = FixMyStreet::DB::Result::Problem->visible_states();
+ unless ( $args{states} ) {
+ $args{states} = FixMyStreet::DB::Result::Problem->visible_states();
}
my $params = {
- state => [ keys %$states ],
+ state => [ keys %{$args{states}} ],
};
- $params->{id} = { -not_in => $ids }
- if $ids;
- $params->{category} = $categories if $categories && @$categories;
+ $params->{id} = { -not_in => $args{ids} }
+ if $args{ids};
+ $params->{category} = $args{categories} if $args{categories} && @{$args{categories}};
+
+ $params->{$c->stash->{report_age_field}} = { '>=', \"current_timestamp-'$args{report_age}'::interval" }
+ if $args{report_age};
FixMyStreet::DB::ResultSet::Problem->non_public_if_possible($params, $c);
$rs = $c->cobrand->problems_restriction($rs);
# Add in any optional extra query parameters
- $params = { %$params, %$extra_params } if $extra_params;
+ $params = { %$params, %{$args{extra}} } if $args{extra};
my $attrs = {
prefetch => 'problem',
- bind => [ $mid_lat, $mid_lon, $dist ],
+ bind => [ $args{latitude}, $args{longitude}, $args{distance} ],
order_by => [ 'distance', { -desc => 'created' } ],
- rows => $limit,
+ rows => $args{limit},
};
my @problems = mySociety::Locale::in_gb_locale { $rs->search( $params, $attrs )->all };
diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm
index ef078ed08..37fc34057 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm
@@ -28,13 +28,23 @@ sub body_query {
sub non_public_if_possible {
my ($rs, $params, $c) = @_;
if ($c->user_exists) {
+ my $only_non_public = $c->stash->{only_non_public} ? 1 : 0;
if ($c->user->is_superuser) {
# See all reports, no restriction
- } elsif ($c->user->has_body_permission_to('report_inspect')) {
- $params->{'-or'} = [
- non_public => 0,
- $rs->body_query($c->user->from_body->id),
- ];
+ $params->{non_public} = 1 if $only_non_public;
+ } elsif ($c->user->has_body_permission_to('report_inspect') ||
+ $c->user->has_body_permission_to('report_mark_private')) {
+ if ($only_non_public) {
+ $params->{'-and'} = [
+ non_public => 1,
+ $rs->body_query($c->user->from_body->id),
+ ];
+ } else {
+ $params->{'-or'} = [
+ non_public => 0,
+ $rs->body_query($c->user->from_body->id),
+ ];
+ }
} else {
$params->{non_public} = 0;
}
@@ -57,6 +67,10 @@ sub to_body {
# Front page statistics
+sub _cache_timeout {
+ FixMyStreet->config('CACHE_TIMEOUT') // 3600;
+}
+
sub recent_fixed {
my $rs = shift;
my $key = "recent_fixed:$site_key";
@@ -66,7 +80,7 @@ sub recent_fixed {
state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
lastupdate => { '>', \"current_timestamp-'1 month'::interval" },
} )->count;
- Memcached::set($key, $result, 3600);
+ Memcached::set($key, $result, _cache_timeout());
}
return $result;
}
@@ -80,7 +94,7 @@ sub number_comments {
{ 'comments.state' => 'confirmed' },
{ join => 'comments' }
)->count;
- Memcached::set($key, $result, 3600);
+ Memcached::set($key, $result, _cache_timeout());
}
return $result;
}
@@ -95,7 +109,7 @@ sub recent_new {
state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
confirmed => { '>', \"current_timestamp-'$interval'::interval" },
} )->count;
- Memcached::set($key, $result, 3600);
+ Memcached::set($key, $result, _cache_timeout());
}
return $result;
}
@@ -144,10 +158,10 @@ sub _recent {
# Need to reattach schema so that confirmed column gets reinflated.
$probs->[0]->result_source->schema( $rs->result_source->schema ) if $probs->[0];
# Catch any cached ones since hidden
- $probs = [ grep { ! $_->is_hidden } @$probs ];
+ $probs = [ grep { $_->photo && ! $_->is_hidden } @$probs ];
} else {
$probs = [ $rs->search( $query, $attrs )->all ];
- Memcached::set($key, $probs, 3600);
+ Memcached::set($key, $probs, _cache_timeout());
}
}
@@ -172,6 +186,9 @@ sub around_map {
latitude => { '>=', $p{min_lat}, '<', $p{max_lat} },
longitude => { '>=', $p{min_lon}, '<', $p{max_lon} },
};
+
+ $q->{$c->stash->{report_age_field}} = { '>=', \"current_timestamp-'$p{report_age}'::interval" } if
+ $p{report_age};
$q->{category} = $p{categories} if $p{categories} && @{$p{categories}};
$rs->non_public_if_possible($q, $c);
@@ -198,9 +215,9 @@ sub timeline {
return $rs->search(
{
-or => {
- created => { '>=', \"current_timestamp-'7 days'::interval" },
- confirmed => { '>=', \"current_timestamp-'7 days'::interval" },
- whensent => { '>=', \"current_timestamp-'7 days'::interval" },
+ 'me.created' => { '>=', \"current_timestamp-'7 days'::interval" },
+ 'me.confirmed' => { '>=', \"current_timestamp-'7 days'::interval" },
+ 'me.whensent' => { '>=', \"current_timestamp-'7 days'::interval" },
}
},
{
diff --git a/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm b/perllib/FixMyStreet/DB/ResultSet/ReportExtraField.pm
index 1348df3c2..9c47b1894 100644
--- a/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/ReportExtraField.pm
@@ -1,4 +1,4 @@
-package FixMyStreet::DB::ResultSet::ReportExtraFields;
+package FixMyStreet::DB::ResultSet::ReportExtraField;
use base 'DBIx::Class::ResultSet';
use strict;
diff --git a/perllib/FixMyStreet/DateRange.pm b/perllib/FixMyStreet/DateRange.pm
new file mode 100644
index 000000000..bc4f4e1af
--- /dev/null
+++ b/perllib/FixMyStreet/DateRange.pm
@@ -0,0 +1,72 @@
+package FixMyStreet::DateRange;
+
+use DateTime;
+use DateTime::Format::Flexible;
+use Moo;
+use Try::Tiny;
+
+my $one_day = DateTime::Duration->new( days => 1 );
+
+has start_date => ( is => 'ro' );
+
+has start_default => ( is => 'ro' );
+
+has end_date => ( is => 'ro' );
+
+has parser => (
+ is => 'ro',
+ default => sub { DateTime::Format::Flexible->new }
+);
+
+has formatter => (
+ is => 'lazy',
+ default => sub {
+ my $self = shift;
+ return $self->parser;
+ }
+);
+
+sub _dt {
+ my ($self, $date) = @_;
+ my %params;
+ $params{european} = 1 if $self->parser->isa('DateTime::Format::Flexible');
+ my $d = try {
+ $self->parser->parse_datetime($date, %params)
+ };
+ return $d;
+}
+
+sub start {
+ my $self = shift;
+ $self->_dt($self->start_date) || $self->start_default
+}
+
+sub end {
+ my $self = shift;
+ my $d = $self->_dt($self->end_date);
+ $d += $one_day if $d;
+ return $d;
+}
+
+sub _formatted {
+ my ($self, $dt) = @_;
+ return unless $dt;
+ $self->formatter->format_datetime($dt);
+}
+
+sub start_formatted { $_[0]->_formatted($_[0]->start) }
+sub end_formatted { $_[0]->_formatted($_[0]->end) }
+
+sub sql {
+ my ($self, $default) = @_;
+ my $sql = {};
+ if (my $start = $self->start_formatted) {
+ $sql->{'>='} = $start;
+ }
+ if (my $end = $self->end_formatted) {
+ $sql->{'<'} = $end;
+ }
+ return $sql;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm
index ea84e3966..2b72b5c63 100644
--- a/perllib/FixMyStreet/Email.pm
+++ b/perllib/FixMyStreet/Email.pm
@@ -22,6 +22,7 @@ use FixMyStreet::Email::Sender;
sub test_dmarc {
my $email = shift;
return if FixMyStreet->test_mode;
+ return 1 if $email =~ /\@swdevon.gov.uk$/;
return Utils::Email::test_dmarc($email);
}
diff --git a/perllib/FixMyStreet/Geocode.pm b/perllib/FixMyStreet/Geocode.pm
index aeac0ab6d..d552afaa5 100644
--- a/perllib/FixMyStreet/Geocode.pm
+++ b/perllib/FixMyStreet/Geocode.pm
@@ -59,7 +59,7 @@ sub string {
sub escape {
my ($s, $c) = @_;
$s = lc($s);
- $s =~ s/[^-&\w ']/ /g;
+ $s =~ s/[^-&\w ',]/ /g;
$s =~ s/\s+/ /g;
$s = URI::Escape::uri_escape_utf8($s);
$s =~ s/%20/+/g;
diff --git a/perllib/FixMyStreet/Geocode/OSM.pm b/perllib/FixMyStreet/Geocode/OSM.pm
index 4d57007c5..0d296f299 100644
--- a/perllib/FixMyStreet/Geocode/OSM.pm
+++ b/perllib/FixMyStreet/Geocode/OSM.pm
@@ -30,7 +30,7 @@ sub string {
$s = $params->{string} if $params->{string};
$s = FixMyStreet::Geocode::escape($s);
- $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i;
+ $s .= '%2C+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i;
my $url = "${nominatimbase}search?";
my %query_params = (
diff --git a/perllib/FixMyStreet/ImageMagick.pm b/perllib/FixMyStreet/ImageMagick.pm
new file mode 100644
index 000000000..af9f56478
--- /dev/null
+++ b/perllib/FixMyStreet/ImageMagick.pm
@@ -0,0 +1,69 @@
+package FixMyStreet::ImageMagick;
+
+use Moo;
+
+my $IM = eval {
+ return 0 if FixMyStreet->test_mode;
+ require Image::Magick;
+ Image::Magick->import;
+ 1;
+};
+
+has blob => ( is => 'ro' );
+
+has image => (
+ is => 'rwp',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ return unless $IM;
+ my $image = Image::Magick->new;
+ $image->BlobToImage($self->blob);
+ return $image;
+ },
+);
+
+sub strip {
+ my $self = shift;
+ return $self unless $self->image;
+ $self->image->Strip();
+ return $self;
+}
+
+sub rotate {
+ my ($self, $direction) = @_;
+ return $self unless $self->image;
+ my $err = $self->image->Rotate($direction);
+ return 0 if $err;
+ return $self;
+}
+
+# Shrinks a picture to the specified size, but keeping in proportion.
+sub shrink {
+ my ($self, $size) = @_;
+ return $self unless $self->image;
+ my $err = $self->image->Scale(geometry => "$size>");
+ throw Error::Simple("resize failed: $err") if "$err";
+ return $self->strip;
+}
+
+# Shrinks a picture to 90x60, cropping so that it is exactly that.
+sub crop {
+ my $self = shift;
+ return $self unless $self->image;
+ my $err = $self->image->Resize( geometry => "90x60^" );
+ throw Error::Simple("resize failed: $err") if "$err";
+ $err = $self->image->Extent( geometry => '90x60', gravity => 'Center' );
+ throw Error::Simple("resize failed: $err") if "$err";
+ return $self->strip;
+}
+
+sub as_blob {
+ my $self = shift;
+ return $self->blob unless $self->image;
+ my @blobs = $self->image->ImageToBlob();
+ $self->_set_image(undef);
+ return $blobs[0];
+}
+
+1;
diff --git a/perllib/FixMyStreet/Map.pm b/perllib/FixMyStreet/Map.pm
index f5d4c1db6..8b8cfe82c 100644
--- a/perllib/FixMyStreet/Map.pm
+++ b/perllib/FixMyStreet/Map.pm
@@ -92,23 +92,33 @@ sub map_features {
$p{latitude} = Utils::truncate_coordinate(($p{max_lat} + $p{min_lat} ) / 2);
}
+ my $report_age = $c->stash->{show_old_reports} ? undef : $c->cobrand->report_age;
+ $p{report_age} = $report_age;
+
$p{page} = $c->get_param('p') || 1;
my $on_map = $c->cobrand->problems_on_map->around_map( $c, %p );
my $pager = $c->stash->{pager} = $on_map->pager;
$on_map = [ $on_map->all ];
- my $dist = FixMyStreet::Gaze::get_radius_containing_population( $p{latitude}, $p{longitude} );
+ if ( $c->{stash}->{show_old_reports} ) {
+ # if show_old_reports is on then there must be old reports
+ $c->stash->{num_old_reports} = 1;
+ } else {
+ my $older = $c->cobrand->problems_on_map->around_map( $c, %p, report_age => undef, page => 1 );
+ $c->stash->{num_old_reports} = $older->pager->total_entries - $pager->total_entries;
+ }
+ # if there are fewer entries than our paging limit on the map then
+ # also return nearby entries for display
my $nearby;
if (@$on_map < $pager->entries_per_page && $pager->current_page == 1) {
- my $limit = 20;
- my @ids = map { $_->id } @$on_map;
- $nearby = $c->model('DB::Nearby')->nearby(
- $c, $dist, \@ids, $limit, @p{"latitude", "longitude", "categories", "states", "extra"}
- );
+ $p{limit} = 20;
+ $p{ids} = [ map { $_->id } @$on_map ];
+ $p{distance} = FixMyStreet::Gaze::get_radius_containing_population( $p{latitude}, $p{longitude} );
+ $nearby = $c->model('DB::Nearby')->nearby($c, %p);
}
- return ( $on_map, $nearby, $dist );
+ return ( $on_map, $nearby );
}
sub click_to_wgs84 {
diff --git a/perllib/FixMyStreet/Map/Angus.pm b/perllib/FixMyStreet/Map/Angus.pm
deleted file mode 100644
index 98f5373c1..000000000
--- a/perllib/FixMyStreet/Map/Angus.pm
+++ /dev/null
@@ -1,18 +0,0 @@
-# FixMyStreet:Map::Angus
-# More JavaScript, for street assets
-
-package FixMyStreet::Map::Angus;
-use base 'FixMyStreet::Map::FMS';
-
-use strict;
-
-sub map_javascript { [
- '/vendor/OpenLayers/OpenLayers.angus.js',
- '/js/map-OpenLayers.js',
- '/js/map-bing-ol.js',
- '/js/map-fms.js',
- '/cobrands/fixmystreet/assets.js',
- '/cobrands/angus/js.js',
-] }
-
-1;
diff --git a/perllib/FixMyStreet/Map/BathNES.pm b/perllib/FixMyStreet/Map/BathNES.pm
index 9c9c3c11d..45261a625 100644
--- a/perllib/FixMyStreet/Map/BathNES.pm
+++ b/perllib/FixMyStreet/Map/BathNES.pm
@@ -7,12 +7,14 @@ use base 'FixMyStreet::Map::OSM';
use strict;
sub map_javascript { [
- '/vendor/OpenLayers/OpenLayers.bathnes.js',
+ '/vendor/OpenLayers/OpenLayers.wfs.js',
'/vendor/OpenLayers.Projection.OrdnanceSurvey.js',
'/js/map-OpenLayers.js',
'/js/map-OpenStreetMap.js',
+ '/cobrands/fixmystreet-uk-councils/roadworks.js',
'/cobrands/fixmystreet/assets.js',
'/cobrands/bathnes/js.js',
+ '/cobrands/bathnes/assets.js',
] }
-1; \ No newline at end of file
+1;
diff --git a/perllib/FixMyStreet/Map/Bing.pm b/perllib/FixMyStreet/Map/Bing.pm
index 68c9fea32..97a0d229f 100644
--- a/perllib/FixMyStreet/Map/Bing.pm
+++ b/perllib/FixMyStreet/Map/Bing.pm
@@ -30,7 +30,7 @@ sub get_quadkey {
}
sub map_tile_base {
- '', "//ecn.%s.tiles.virtualearth.net/tiles/r%s.png?g=5941";
+ '', "//ecn.%s.tiles.virtualearth.net/tiles/r%s.png?g=6570";
}
sub map_tiles {
diff --git a/perllib/FixMyStreet/Map/Bristol.pm b/perllib/FixMyStreet/Map/Bristol.pm
index 5d05fbd34..99bdd26d7 100644
--- a/perllib/FixMyStreet/Map/Bristol.pm
+++ b/perllib/FixMyStreet/Map/Bristol.pm
@@ -64,7 +64,7 @@ sub map_javascript { [
'/js/map-wmts-base.js',
'/js/map-wmts-bristol.js',
'/cobrands/fixmystreet/assets.js',
- '/cobrands/bristol/js.js',
+ '/cobrands/bristol/assets.js',
] }
# Reproject a WGS84 lat/lon into BNG easting/northing
diff --git a/perllib/FixMyStreet/Map/Bromley.pm b/perllib/FixMyStreet/Map/Bromley.pm
index 22e4147f6..cd50cc1d1 100644
--- a/perllib/FixMyStreet/Map/Bromley.pm
+++ b/perllib/FixMyStreet/Map/Bromley.pm
@@ -10,12 +10,13 @@ use base 'FixMyStreet::Map::FMS';
use strict;
sub map_javascript { [
- '/vendor/OpenLayers/OpenLayers.buckinghamshire.js',
+ '/vendor/OpenLayers/OpenLayers.wfs.js',
'/js/map-OpenLayers.js',
'/js/map-bing-ol.js',
'/js/map-fms.js',
'/cobrands/fixmystreet/assets.js',
'/cobrands/bromley/map.js',
+ '/cobrands/bromley/assets.js',
] }
sub map_tile_base {
diff --git a/perllib/FixMyStreet/Map/Buckinghamshire.pm b/perllib/FixMyStreet/Map/Buckinghamshire.pm
index b6d86d4b9..10ee2a080 100644
--- a/perllib/FixMyStreet/Map/Buckinghamshire.pm
+++ b/perllib/FixMyStreet/Map/Buckinghamshire.pm
@@ -7,13 +7,14 @@ use base 'FixMyStreet::Map::OSM';
use strict;
sub map_javascript { [
- '/vendor/OpenLayers/OpenLayers.buckinghamshire.js',
+ '/vendor/OpenLayers/OpenLayers.wfs.js',
'/vendor/OpenLayers.Projection.OrdnanceSurvey.js',
'/js/map-OpenLayers.js',
'/js/map-OpenStreetMap.js',
'/cobrands/fixmystreet-uk-councils/roadworks.js',
'/cobrands/fixmystreet/assets.js',
'/cobrands/buckinghamshire/js.js',
+ '/cobrands/buckinghamshire/assets.js',
] }
1;
diff --git a/perllib/FixMyStreet/Map/FMS.pm b/perllib/FixMyStreet/Map/FMS.pm
index 13c7f9d87..126fc34bf 100644
--- a/perllib/FixMyStreet/Map/FMS.pm
+++ b/perllib/FixMyStreet/Map/FMS.pm
@@ -12,14 +12,14 @@ use strict;
sub map_template { 'fms' }
sub map_javascript { [
- '/vendor/OpenLayers/OpenLayers.fixmystreet.js',
+ '/vendor/OpenLayers/OpenLayers.wfs.js',
'/js/map-OpenLayers.js',
'/js/map-bing-ol.js',
'/js/map-fms.js',
] }
sub map_tile_base {
- '-', "//%stilma.mysociety.org/sv/%d/%d/%d.png";
+ '-', "//%stilma.mysociety.org/oml/%d/%d/%d.png";
}
sub map_tiles {
@@ -36,8 +36,8 @@ sub map_tiles {
];
} else {
my $key = FixMyStreet->config('BING_MAPS_API_KEY');
- my $url = "g=5941";
- $url .= "&productSet=mmOS&key=$key" if $z > 10 && !$ni;
+ my $url = "g=6570";
+ $url .= "&productSet=mmOS&key=$key" if $z > 11 && !$ni;
return [
"//ecn.t0.tiles.virtualearth.net/tiles/r" . $self->get_quadkey($x-1, $y-1, $z) . ".png?$url",
"//ecn.t1.tiles.virtualearth.net/tiles/r" . $self->get_quadkey($x, $y-1, $z) . ".png?$url",
diff --git a/perllib/FixMyStreet/Map/Google.pm b/perllib/FixMyStreet/Map/Google.pm
index f40eff167..c1fb05e43 100644
--- a/perllib/FixMyStreet/Map/Google.pm
+++ b/perllib/FixMyStreet/Map/Google.pm
@@ -44,6 +44,7 @@ sub display_map {
if defined $c->get_param('lat');
$params{longitude} = Utils::truncate_coordinate($c->get_param('lon') + 0)
if defined $c->get_param('lon');
+ $params{zoomToBounds} = $params{any_zoom} && !defined $c->get_param('zoom');
my $zoom = defined $c->get_param('zoom') ? $c->get_param('zoom') + 0 : $default_zoom;
$zoom = $numZoomLevels - 1 if $zoom >= $numZoomLevels;
diff --git a/perllib/FixMyStreet/Map/Lincolnshire.pm b/perllib/FixMyStreet/Map/Lincolnshire.pm
new file mode 100644
index 000000000..7dbfe5d8e
--- /dev/null
+++ b/perllib/FixMyStreet/Map/Lincolnshire.pm
@@ -0,0 +1,21 @@
+# FixMyStreet:Map::Lincolnshire
+# More JavaScript, for street assets
+
+package FixMyStreet::Map::Lincolnshire;
+use base 'FixMyStreet::Map::FMS';
+
+use strict;
+
+sub map_javascript { [
+ '/vendor/OpenLayers/OpenLayers.wfs.js',
+ '/vendor/OpenLayers.Projection.OrdnanceSurvey.js',
+ '/js/map-OpenLayers.js',
+ '/js/map-bing-ol.js',
+ '/js/map-fms.js',
+ '/cobrands/fixmystreet-uk-councils/roadworks.js',
+ '/cobrands/fixmystreet/assets.js',
+ '/cobrands/lincolnshire/roadworks.js',
+ '/cobrands/lincolnshire/assets.js',
+] }
+
+1;
diff --git a/perllib/FixMyStreet/Map/OSM.pm b/perllib/FixMyStreet/Map/OSM.pm
index 47d6eeee7..a6cb6acea 100644
--- a/perllib/FixMyStreet/Map/OSM.pm
+++ b/perllib/FixMyStreet/Map/OSM.pm
@@ -19,7 +19,7 @@ sub map_type { 'OpenLayers.Layer.OSM.Mapnik' }
sub map_template { 'osm' }
sub map_javascript { [
- '/vendor/OpenLayers/OpenLayers.fixmystreet.js',
+ '/vendor/OpenLayers/OpenLayers.wfs.js',
'/js/map-OpenLayers.js',
'/js/map-OpenStreetMap.js',
] }
@@ -57,6 +57,7 @@ sub display_map {
if defined $c->get_param('lat');
$params{longitude} = Utils::truncate_coordinate($c->get_param('lon') + 0)
if defined $c->get_param('lon');
+ $params{zoomToBounds} = $params{any_zoom} && !defined $c->get_param('zoom');
my %data;
$data{cobrand} = $c->cobrand;
@@ -69,8 +70,8 @@ sub display_map {
sub generate_map_data {
my ($self, $data, %params) = @_;
- my $numZoomLevels = ZOOM_LEVELS;
- my $zoomOffset = MIN_ZOOM_LEVEL;
+ my $numZoomLevels = $self->ZOOM_LEVELS;
+ my $zoomOffset = $self->MIN_ZOOM_LEVEL;
if ($params{any_zoom}) {
$numZoomLevels = 19;
$zoomOffset = 0;
diff --git a/perllib/FixMyStreet/Map/WMTSBase.pm b/perllib/FixMyStreet/Map/WMTSBase.pm
index 960a58a41..051f8f369 100644
--- a/perllib/FixMyStreet/Map/WMTSBase.pm
+++ b/perllib/FixMyStreet/Map/WMTSBase.pm
@@ -205,15 +205,15 @@ sub get_map_hash {
numZoomLevels => $self->zoom_parameters->{zoom_levels},
tile_size => $self->tile_parameters->{size},
tile_dpi => $self->tile_parameters->{dpi},
- tile_urls => encode_json $self->tile_parameters->{urls},
+ tile_urls => encode_json( $self->tile_parameters->{urls} ),
tile_suffix => $self->tile_parameters->{suffix},
- layer_names => encode_json $self->tile_parameters->{layer_names},
+ layer_names => encode_json( $self->tile_parameters->{layer_names} ),
layer_style => $self->tile_parameters->{layer_style},
matrix_set => $self->tile_parameters->{matrix_set},
map_projection => $self->tile_parameters->{projection},
origin_x => force_float_format($self->tile_parameters->{origin_x}),
origin_y => force_float_format($self->tile_parameters->{origin_y}),
- scales => encode_json \@scales,
+ scales => encode_json( \@scales ),
};
}
diff --git a/perllib/FixMyStreet/Map/Zurich.pm b/perllib/FixMyStreet/Map/Zurich.pm
index 6d9a309ff..857d8a826 100644
--- a/perllib/FixMyStreet/Map/Zurich.pm
+++ b/perllib/FixMyStreet/Map/Zurich.pm
@@ -22,8 +22,8 @@ sub tile_parameters {
my $self = shift;
my $params = {
urls => [
- 'http://www.ogc.stadt-zuerich.ch/mapproxy/wmts/',
- 'http://www.ogc.stadt-zuerich.ch/mapproxy/wmts/',
+ 'https://www.ogc.stadt-zuerich.ch/mapproxy/wmts/',
+ 'https://www.ogc.stadt-zuerich.ch/mapproxy/wmts/',
],
layer_names => [ 'LuftbildHybrid', 'Stadtplan3D' ],
wmts_version => '1.0.0',
diff --git a/perllib/FixMyStreet/MapIt.pm b/perllib/FixMyStreet/MapIt.pm
new file mode 100644
index 000000000..d0a5f4760
--- /dev/null
+++ b/perllib/FixMyStreet/MapIt.pm
@@ -0,0 +1,18 @@
+package FixMyStreet::MapIt;
+
+use FixMyStreet;
+use mySociety::MaPit;
+
+sub call {
+ my ($url, $params, %opts) = @_;
+
+ # 'area' always returns the ID you provide, no matter its generation, so no
+ # point in specifying it for that. 'areas' similarly if given IDs, but we
+ # might be looking up types or names, so might as well specify it then.
+ $opts{generation} = FixMyStreet->config('MAPIT_GENERATION')
+ if $url ne 'area' && FixMyStreet->config('MAPIT_GENERATION');
+
+ return mySociety::MaPit::call($url, $params, %opts);
+}
+
+1;
diff --git a/perllib/FixMyStreet/PhotoStorage.pm b/perllib/FixMyStreet/PhotoStorage.pm
new file mode 100644
index 000000000..a441fb718
--- /dev/null
+++ b/perllib/FixMyStreet/PhotoStorage.pm
@@ -0,0 +1,41 @@
+package FixMyStreet::PhotoStorage;
+
+use Moose;
+use Digest::SHA qw(sha1_hex);
+use Module::Load;
+use FixMyStreet;
+
+our $instance; # our, so tests can set to undef when testing different backends
+sub backend {
+ return $instance if $instance;
+ my $class = 'FixMyStreet::PhotoStorage::';
+ $class .= FixMyStreet->config('PHOTO_STORAGE_BACKEND') || 'FileSystem';
+ load $class;
+ $instance = $class->new();
+ return $instance;
+}
+
+sub detect_type {
+ my ($self, $photo) = @_;
+ return 'jpeg' if $photo =~ /^\x{ff}\x{d8}/;
+ return 'png' if $photo =~ /^\x{89}\x{50}/;
+ return 'tiff' if $photo =~ /^II/;
+ return 'gif' if $photo =~ /^GIF/;
+ return '';
+}
+
+=head2 get_fileid
+
+Calculates an identifier for a binary blob of photo data.
+This is just the SHA1 hash of the blob currently.
+
+=cut
+
+sub get_fileid {
+ my ($self, $photo_blob) = @_;
+ return sha1_hex($photo_blob);
+}
+
+
+
+1;
diff --git a/perllib/FixMyStreet/PhotoStorage/FileSystem.pm b/perllib/FixMyStreet/PhotoStorage/FileSystem.pm
new file mode 100644
index 000000000..1d3fe5cfd
--- /dev/null
+++ b/perllib/FixMyStreet/PhotoStorage/FileSystem.pm
@@ -0,0 +1,112 @@
+package FixMyStreet::PhotoStorage::FileSystem;
+
+use Moose;
+use parent 'FixMyStreet::PhotoStorage';
+
+use Path::Tiny 'path';
+
+
+has upload_dir => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $cfg = FixMyStreet->config('PHOTO_STORAGE_OPTIONS');
+ my $dir = $cfg ? $cfg->{UPLOAD_DIR} : FixMyStreet->config('UPLOAD_DIR');
+ return path($dir)->absolute(FixMyStreet->path_to());
+ },
+);
+
+=head2 init
+
+Creates UPLOAD_DIR and checks it's writeable.
+
+=cut
+
+sub init {
+ my $self = shift;
+ my $cache_dir = $self->upload_dir;
+ $cache_dir->mkpath;
+ unless ( -d $cache_dir && -w $cache_dir ) {
+ warn "\x1b[31mCan't find/write to photo cache directory '$cache_dir'\x1b[0m\n";
+ return;
+ }
+ return 1;
+}
+
+=head2 get_file
+
+Returns a Path::Tiny path to a file on disk identified by an ID and type.
+File may or may not exist. This handle is then used to read photo data or
+write to disk.
+
+=cut
+
+sub get_file {
+ my ($self, $fileid, $type) = @_;
+ my $cache_dir = $self->upload_dir;
+ return path( $cache_dir, "$fileid.$type" );
+}
+
+
+=head2 store_photo
+
+Stores a blob of binary data representing a photo on disk.
+Returns a key which is used in the future to get the contents of the file.
+
+=cut
+
+sub store_photo {
+ my ($self, $photo_blob) = @_;
+
+ my $type = $self->detect_type($photo_blob) || 'jpeg';
+ my $fileid = $self->get_fileid($photo_blob);
+ my $file = $self->get_file($fileid, $type);
+ $file->spew_raw($photo_blob);
+
+ return $file->basename;
+}
+
+
+=head2 retrieve_photo
+
+Fetches the file content of a particular photo from storage.
+Returns the binary blob, the filetype, and the file path, if
+the photo exists in storage.
+
+=cut
+
+sub retrieve_photo {
+ my ($self, $filename) = @_;
+
+ my ($fileid, $type) = split /\./, $filename;
+ my $file = $self->get_file($fileid, $type);
+ if ($file->exists) {
+ my $photo = $file->slurp_raw;
+ return ($photo, $type, $file);
+ }
+}
+
+
+=head2 validate_key
+
+A long-running FMS instance might have reports whose photo IDs in the DB
+don't include the file extension. This function takes a value from the DB and
+returns a 'tidied' version that can be used when calling photo_exists
+or retrieve_photo.
+
+If the passed key doesn't seem like it'll result in a valid filename (i.e.
+it's not a 40-char SHA1 hash) returns undef.
+
+=cut
+
+sub validate_key {
+ my ($self, $key) = @_;
+
+ my ($fileid, $type) = split /\./, $key;
+ $type ||= 'jpeg';
+ if ($fileid && length($fileid) == 40) {
+ return "$fileid.$type";
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/PhotoStorage/S3.pm b/perllib/FixMyStreet/PhotoStorage/S3.pm
new file mode 100644
index 000000000..45325e9dc
--- /dev/null
+++ b/perllib/FixMyStreet/PhotoStorage/S3.pm
@@ -0,0 +1,122 @@
+package FixMyStreet::PhotoStorage::S3;
+
+use Moose;
+use parent 'FixMyStreet::PhotoStorage';
+
+use Net::Amazon::S3;
+use Try::Tiny;
+
+
+has client => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $key = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{ACCESS_KEY};
+ my $secret = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{SECRET_KEY};
+
+ my $s3 = Net::Amazon::S3->new(
+ aws_access_key_id => $key,
+ aws_secret_access_key => $secret,
+ retry => 1,
+ );
+ return Net::Amazon::S3::Client->new( s3 => $s3 );
+ },
+);
+
+has bucket => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ shift->client->bucket( name => FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{BUCKET} );
+ },
+);
+
+has region => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ return FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{REGION};
+ },
+);
+
+has prefix => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $prefix = FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{PREFIX};
+ return "" unless $prefix;
+ $prefix =~ s#/$##;
+ return "$prefix/";
+ },
+);
+
+sub init {
+ my $self = shift;
+
+ return 1 if $self->_bucket_exists();
+
+ if ( FixMyStreet->config('PHOTO_STORAGE_OPTIONS')->{CREATE_BUCKET} ) {
+ my $name = $self->bucket->name;
+ try {
+ $self->client->create_bucket(
+ name => $name,
+ location_constraint => $self->region,
+ );
+ } catch {
+ warn "\x1b[31mCouldn't create S3 bucket '$name'\x1b[0m\n";
+ return;
+ };
+
+ return 1 if $self->_bucket_exists();
+
+ warn "\x1b[31mCouldn't create S3 bucket '$name'\x1b[0m\n";
+ return;
+ } else {
+ my $bucket = $self->bucket->name;
+ warn "\x1b[31mS3 bucket '$bucket' doesn't exist and CREATE_BUCKET is not set.\x1b[0m\n";
+ return;
+ }
+}
+
+sub _bucket_exists {
+ my $self = shift;
+ my $name = $self->bucket->name;
+ my @buckets = $self->client->buckets;
+ return grep { $_->name eq $name } @buckets;
+}
+
+sub get_object {
+ my ($self, $key) = @_;
+ return $self->bucket->object( key => $key );
+}
+
+sub store_photo {
+ my ($self, $photo_blob) = @_;
+
+ my $type = $self->detect_type($photo_blob) || 'jpeg';
+ my $fileid = $self->get_fileid($photo_blob);
+ my $key = $self->prefix . "$fileid.$type";
+
+ my $object = $self->get_object($key);
+ $object->put($photo_blob);
+
+ return $key;
+}
+
+
+sub retrieve_photo {
+ my ($self, $key) = @_;
+
+ my $object = $self->get_object($key);
+ if ($object->exists) {
+ my ($fileid, $type) = split /\./, $key;
+ return ($object->get, $type);
+ }
+
+}
+
+sub validate_key { $_[1] }
+
+
+1;
diff --git a/perllib/FixMyStreet/Roles/Abuser.pm b/perllib/FixMyStreet/Roles/Abuser.pm
index e2e9eb19e..7510e6bc2 100644
--- a/perllib/FixMyStreet/Roles/Abuser.pm
+++ b/perllib/FixMyStreet/Roles/Abuser.pm
@@ -14,7 +14,8 @@ sub is_from_abuser {
my $self = shift;
my $email = $self->user->email;
- my ($domain) = $email =~ m{ @ (.*) \z }x if $email;
+ my $domain;
+ ($domain) = $email =~ m{ @ (.*) \z }x if $email;
my $phone = $self->user->phone;
# search for an entry in the abuse table
diff --git a/perllib/FixMyStreet/Roles/ConfirmValidation.pm b/perllib/FixMyStreet/Roles/ConfirmValidation.pm
new file mode 100644
index 000000000..776230287
--- /dev/null
+++ b/perllib/FixMyStreet/Roles/ConfirmValidation.pm
@@ -0,0 +1,38 @@
+package FixMyStreet::Roles::ConfirmValidation;
+use Moo::Role;
+
+=head1 NAME
+
+FixMyStreet::Roles::ConfirmValidation - role for adding standard confirm validation
+
+=head1 SYNOPSIS
+
+This is applied to a Cobrand class to add validation of reports using standard
+Confirm field lengths.
+
+ use Moo;
+ with 'FixMyStreet::Roles::ConfirmValidation';
+
+=cut
+
+has max_report_length => ( is => 'ro', default => 2000 );
+
+sub report_validation {
+ my ($self, $report, $errors) = @_;
+
+ if ( length( $report->name ) > 50 ) {
+ $errors->{name} = sprintf( _('Names are limited to %d characters in length.'), 50 );
+ }
+
+ if ( length( $report->user->phone ) > 20 ) {
+ $errors->{phone} = sprintf( _('Phone numbers are limited to %s characters in length.'), 20 );
+ }
+
+ if ( length( $report->detail ) > $self->max_report_length ) {
+ $errors->{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), $self->max_report_length );
+ }
+
+ return $errors;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Roles/Moderation.pm b/perllib/FixMyStreet/Roles/Moderation.pm
new file mode 100644
index 000000000..fb9ea3a70
--- /dev/null
+++ b/perllib/FixMyStreet/Roles/Moderation.pm
@@ -0,0 +1,47 @@
+package FixMyStreet::Roles::Moderation;
+use Moo::Role;
+
+=head2 latest_moderation_log_entry
+
+Return most recent AdminLog object concerning moderation
+
+=cut
+
+sub latest_moderation {
+ my $self = shift;
+
+ return $self->moderation_original_datas->search(
+ $self->moderation_filter,
+ { order_by => { -desc => 'id' } })->first;
+}
+
+sub latest_moderation_log_entry {
+ my $self = shift;
+
+ my $latest = $self->latest_moderation;
+ return unless $latest;
+
+ my $rs = $self->result_source->schema->resultset("AdminLog");
+ my $log = $rs->search({
+ object_id => $latest->id,
+ object_type => 'moderation',
+ })->first;
+ return $log if $log;
+
+ return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => { -desc => 'id' } })->first;
+}
+
+=head2 moderation_history
+
+Returns all moderation history, most recent first.
+
+=cut
+
+sub moderation_history {
+ my $self = shift;
+ return $self->moderation_original_datas->search(
+ $self->moderation_filter,
+ { order_by => { -desc => 'id' } })->all;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Roles/PhotoSet.pm b/perllib/FixMyStreet/Roles/PhotoSet.pm
index 2a6863cff..4a40ef3f9 100644
--- a/perllib/FixMyStreet/Roles/PhotoSet.pm
+++ b/perllib/FixMyStreet/Roles/PhotoSet.pm
@@ -19,9 +19,8 @@ Return a PhotoSet object for all photos attached to this field
sub get_photoset {
my ($self) = @_;
- my $class = 'FixMyStreet::App::Model::PhotoSet';
- eval "use $class";
- return $class->new({
+ require FixMyStreet::App::Model::PhotoSet;
+ return FixMyStreet::App::Model::PhotoSet->new({
db_data => $self->photo,
object => $self,
});
diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm
index 4b5641f9e..55f4b3db5 100644
--- a/perllib/FixMyStreet/Script/Alerts.pm
+++ b/perllib/FixMyStreet/Script/Alerts.pm
@@ -8,7 +8,7 @@ use IO::String;
use FixMyStreet::Gaze;
use mySociety::Locale;
-use mySociety::MaPit;
+use FixMyStreet::MapIt;
use RABX;
use FixMyStreet::Cobrand;
@@ -185,12 +185,15 @@ sub send() {
# Get a report object for its photo and static map
$data{report} = $schema->resultset('Problem')->find({ id => $row->{id} });
}
- if ($ref eq 'area_problems' || $ref eq 'council_problems' || $ref eq 'ward_problems') {
- my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter});
+ if ($ref eq 'area_problems') {
+ my $va_info = FixMyStreet::MapIt::call('area', $row->{alert_parameter});
$data{area_name} = $va_info->{name};
+ } elsif ($ref eq 'council_problems' || $ref eq 'ward_problems') {
+ my $body = FixMyStreet::DB->resultset('Body')->find({ id => $row->{alert_parameter} });
+ $data{area_name} = $body->name;
}
if ($ref eq 'ward_problems') {
- my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter2});
+ my $va_info = FixMyStreet::MapIt::call('area', $row->{alert_parameter2});
$data{ward_name} = $va_info->{name};
}
}
diff --git a/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm b/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm
index 03bc511a0..0c938682d 100644
--- a/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm
+++ b/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm
@@ -1,11 +1,8 @@
package FixMyStreet::Script::ArchiveOldEnquiries;
-use strict;
+use v5.14;
use warnings;
-require 5.8.0;
-use FixMyStreet;
-use FixMyStreet::App;
use FixMyStreet::DB;
use FixMyStreet::Cobrand;
use FixMyStreet::Map;
@@ -17,17 +14,17 @@ my $opts = {
};
sub query {
- return {
- bodies_str => { 'LIKE', "%".$opts->{body}."%"},
- -and => [
+ my $rs = shift;
+ return $rs->to_body($opts->{body})->search({
+ -and => [
lastupdate => { '<', $opts->{email_cutoff} },
lastupdate => { '>', $opts->{closure_cutoff} },
],
- state => [ FixMyStreet::DB::Result::Problem->open_states() ],
- };
+ state => [ FixMyStreet::DB::Result::Problem->open_states() ],
+ });
}
-sub archive {
+sub update_options {
my $params = shift;
if ( $params ) {
$opts = {
@@ -35,13 +32,19 @@ sub archive {
%$params,
};
}
+}
+
+sub archive {
+ my $params = shift;
+ update_options($params);
unless ( $opts->{commit} ) {
printf "Doing a dry run; emails won't be sent and reports won't be closed.\n";
printf "Re-run with --commit to actually archive reports.\n\n";
}
- my @user_ids = FixMyStreet::DB->resultset('Problem')->search(query(),
+ my $rs = FixMyStreet::DB->resultset('Problem');
+ my @user_ids = query($rs)->search(undef,
{
distinct => 1,
columns => ['user_id'],
@@ -55,7 +58,7 @@ sub archive {
});
my $user_count = $users->count;
- my $problem_count = FixMyStreet::DB->resultset('Problem')->search(query(),
+ my $problem_count = query($rs)->search(undef,
{
columns => ['id'],
rows => $opts->{limit},
@@ -71,8 +74,7 @@ sub archive {
}
}
- my $problems_to_close = FixMyStreet::DB->resultset('Problem')->search({
- bodies_str => { 'LIKE', "%".$opts->{body}."%"},
+ my $problems_to_close = $rs->to_body($opts->{body})->search({
lastupdate => { '<', $opts->{closure_cutoff} },
state => [ FixMyStreet::DB::Result::Problem->open_states() ],
}, {
@@ -87,7 +89,8 @@ sub archive {
sub send_email_and_close {
my ($user) = @_;
- my $problems = $user->problems->search(query(), {
+ my $problems = $user->problems;
+ $problems = query($problems)->search(undef, {
order_by => { -desc => 'confirmed' },
});
@@ -135,22 +138,36 @@ sub close_problems {
return unless $opts->{commit};
my $problems = shift;
+
+ my $extra = { auto_closed_by_script => 1 };
+ $extra->{is_superuser} = 1 if !$opts->{user_name};
+
+ my $cobrand;
while (my $problem = $problems->next) {
+ # need to do this in case no reports were closed with an
+ # email in which case we won't have set the lang and domain
+ if ($opts->{cobrand} && !$cobrand) {
+ $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($opts->{cobrand})->new();
+ $cobrand->set_lang_and_domain($problem->lang, 1);
+ }
+
my $timestamp = \'current_timestamp';
my $comment = $problem->add_to_comments( {
- text => '',
+ text => $opts->{closure_text} || '',
created => $timestamp,
confirmed => $timestamp,
user_id => $opts->{user},
- name => _('an administrator'),
+ name => $opts->{user_name} || _('an administrator'),
mark_fixed => 0,
anonymous => 0,
state => 'confirmed',
problem_state => 'closed',
- extra => { is_superuser => 1 },
+ extra => $extra,
} );
$problem->update({ state => 'closed', send_questionnaire => 0 });
+ next if $opts->{retain_alerts};
+
# Stop any alerts being sent out about this closure.
my @alerts = FixMyStreet::DB->resultset('Alert')->search( {
alert_type => 'new_updates',
diff --git a/perllib/FixMyStreet/Script/Questionnaires.pm b/perllib/FixMyStreet/Script/Questionnaires.pm
index 5fc01512d..aab4b9b75 100644
--- a/perllib/FixMyStreet/Script/Questionnaires.pm
+++ b/perllib/FixMyStreet/Script/Questionnaires.pm
@@ -43,14 +43,19 @@ sub send_questionnaires_period {
while (my $row = $unsent->next) {
- my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new();
+ my $cobrand = $row->get_cobrand_logged;
$cobrand->set_lang_and_domain($row->lang, 1);
FixMyStreet::Map::set_map_class($cobrand->map_type);
# Not all cobrands send questionnaires
next unless $cobrand->send_questionnaires;
- if ($row->is_from_abuser || !$row->user->email_verified) {
+ # Cobrands can also override sending per row if they wish
+ my $cobrand_send = $cobrand->call_hook('send_questionnaire', $row) // 1;
+
+ if ($row->is_from_abuser || !$row->user->email_verified ||
+ !$cobrand_send || $row->is_closed
+ ) {
$row->update( { send_questionnaire => 0 } );
next;
}
diff --git a/perllib/FixMyStreet/Script/Reports.pm b/perllib/FixMyStreet/Script/Reports.pm
index 578d966d6..ecd461cd9 100644
--- a/perllib/FixMyStreet/Script/Reports.pm
+++ b/perllib/FixMyStreet/Script/Reports.pm
@@ -8,7 +8,6 @@ use DateTime::Format::Pg;
use Utils;
use Utils::OpenStreetMap;
-use mySociety::MaPit;
use FixMyStreet;
use FixMyStreet::Cobrand;
@@ -44,7 +43,7 @@ sub send(;$) {
debug_print("starting to loop through unsent problem reports...") if $debug_mode;
while (my $row = $unsent->next) {
- my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new();
+ my $cobrand = $row->get_cobrand_logged;
FixMyStreet::DB->schema->cobrand($cobrand);
if ($debug_mode) {
@@ -127,10 +126,19 @@ sub send(;$) {
$missing = join(' / ', @missing) if @missing;
}
+ my $send_confirmation_email = $cobrand->report_sent_confirmation_email;
+
my @dear;
my %reporters = ();
my $skip = 0;
while (my $body = $bodies->next) {
+ # See if this body wants confirmation email (in case report made on national site, for example)
+ if (my $cobrand_body = $body->get_cobrand_handler) {
+ if (my $id_ref = $cobrand_body->report_sent_confirmation_email) {
+ $send_confirmation_email = $id_ref;
+ }
+ }
+
my $sender_info = $cobrand->get_body_sender( $body, $row->category );
my $sender = "FixMyStreet::SendReport::" . $sender_info->{method};
@@ -140,7 +148,9 @@ sub send(;$) {
}
$reporters{ $sender } ||= $sender->new();
- my $inspection_required = $sender_info->{contact}->get_extra_metadata('inspection_required') if $sender_info->{contact};
+ my $inspection_required = $sender_info->{contact}
+ ? $sender_info->{contact}->get_extra_metadata('inspection_required')
+ : undef;
if ( $inspection_required ) {
my $reputation_threshold = $sender_info->{contact}->get_extra_metadata('reputation_threshold') || 0;
my $reputation_threshold_met = 0;
@@ -211,12 +221,13 @@ sub send(;$) {
# Multiply results together, so one success counts as a success.
my $result = -1;
+ my @methods;
for my $sender ( keys %reporters ) {
debug_print("sending using " . $sender, $row->id) if $debug_mode;
$sender = $reporters{$sender};
my $res = $sender->send( $row, \%h );
$result *= $res;
- $row->add_send_method($sender) if !$res;
+ push @methods, $sender if !$res;
if ( $sender->unconfirmed_counts) {
foreach my $e (keys %{ $sender->unconfirmed_counts } ) {
foreach my $c (keys %{ $sender->unconfirmed_counts->{$e} }) {
@@ -229,12 +240,19 @@ sub send(;$) {
if FixMyStreet->test_mode && $sender->can('open311_test_req_used');
}
+ # Add the send methods now because e.g. Open311
+ # send() calls $row->discard_changes
+ foreach (@methods) {
+ $row->add_send_method($_);
+ }
+
unless ($result) {
$row->update( {
whensent => \'current_timestamp',
lastupdate => \'current_timestamp',
} );
- if ( $cobrand->report_sent_confirmation_email && !$h{anonymous_report}) {
+ if ($send_confirmation_email && !$h{anonymous_report}) {
+ $h{sent_confirm_id_ref} = $row->$send_confirmation_email;
_send_report_sent_email( $row, \%h, $nomail, $cobrand );
}
debug_print("send successful: OK", $row->id) if $debug_mode;
@@ -274,7 +292,7 @@ sub send(;$) {
} );
while (my $row = $unsent->next) {
my $base_url = FixMyStreet->config('BASE_URL');
- $sending_errors .= "* " . $base_url . "/report/" . $row->id . ", failed "
+ $sending_errors .= "\n" . '=' x 80 . "\n\n" . "* " . $base_url . "/report/" . $row->id . ", failed "
. $row->send_fail_count . " times, last at " . $row->send_fail_timestamp
. ", reason " . $row->send_fail_reason . "\n";
}
@@ -304,7 +322,6 @@ sub _send_report_sent_email {
$h,
{
To => $row->user->email,
- From => [ $cobrand->contact_email, $cobrand->contact_name ],
},
undef,
$nomail,
diff --git a/perllib/FixMyStreet/Script/UpdateAllReports.pm b/perllib/FixMyStreet/Script/UpdateAllReports.pm
index 21d8d28a0..33665b9da 100755
--- a/perllib/FixMyStreet/Script/UpdateAllReports.pm
+++ b/perllib/FixMyStreet/Script/UpdateAllReports.pm
@@ -99,13 +99,14 @@ sub generate {
}
sub end_period {
- my $period = shift;
- FixMyStreet->set_time_zone(DateTime->now)->truncate(to => $period)->add($period . 's' => 1)->subtract(seconds => 1);
+ my ($period, $end) = @_;
+ $end ||= DateTime->now;
+ FixMyStreet->set_time_zone($end)->truncate(to => $period)->add($period . 's' => 1)->subtract(seconds => 1);
}
sub loop_period {
- my ($date, $period, $extra) = @_;
- my $end = end_period($period);
+ my ($date, $extra, $period, $end) = @_;
+ $end = end_period($period, $end);
my @out;
while ($date <= $end) {
push @out, { n => $date->$period, $extra ? (d => $date->$extra) : () };
@@ -114,6 +115,21 @@ sub loop_period {
return @out;
}
+sub get_period_group {
+ my ($start, $end) = @_;
+ my ($group_by, $extra);
+ if (DateTime::Duration->compare($end - $start, DateTime::Duration->new(months => 1)) < 0) {
+ $group_by = 'day';
+ } elsif (DateTime::Duration->compare($end - $start, DateTime::Duration->new(years => 1)) < 0) {
+ $group_by = 'month';
+ $extra = 'month_abbr';
+ } else {
+ $group_by = 'year';
+ }
+
+ return ($group_by, $extra);
+}
+
sub generate_dashboard {
my $body = shift;
@@ -138,16 +154,8 @@ sub generate_dashboard {
$min_confirmed = FixMyStreet->set_time_zone(DateTime->now)->truncate(to => 'day');
}
- my ($group_by, $extra);
- if (DateTime::Duration->compare($end_today - $min_confirmed, DateTime::Duration->new(months => 1)) < 0) {
- $group_by = 'day';
- } elsif (DateTime::Duration->compare($end_today - $min_confirmed, DateTime::Duration->new(years => 1)) < 0) {
- $group_by = 'month';
- $extra = 'month_abbr';
- } else {
- $group_by = 'year';
- }
- my @problem_periods = loop_period($min_confirmed, $group_by, $extra);
+ my ($group_by, $extra) = get_period_group($min_confirmed, $end_today);
+ my @problem_periods = loop_period($min_confirmed, $extra, $group_by);
my %problems_reported_by_period = stuff_by_day_or_year(
$group_by, $rs,
@@ -261,7 +269,7 @@ sub calculate_top_five_bodies {
my $bodies = FixMyStreet::DB->resultset('Body')->search;
while (my $body = $bodies->next) {
- my $avg = $body->calculate_average;
+ my $avg = $body->calculate_average($cobrand_cls->call_hook("body_responsiveness_threshold"));
push @top_five_bodies, { name => $body->name, days => int($avg / 60 / 60 / 24 + 0.5) }
if defined $avg;
}
diff --git a/perllib/FixMyStreet/SendReport.pm b/perllib/FixMyStreet/SendReport.pm
index 2739e3043..db95850e6 100644
--- a/perllib/FixMyStreet/SendReport.pm
+++ b/perllib/FixMyStreet/SendReport.pm
@@ -6,6 +6,7 @@ use MooX::Types::MooseLike::Base qw(:all);
use Module::Pluggable
sub_name => 'senders',
search_path => __PACKAGE__,
+ except => 'FixMyStreet::SendReport::Email::SingleBodyOnly',
require => 1;
has 'body_config' => ( is => 'rw', isa => HashRef, default => sub { {} } );
diff --git a/perllib/FixMyStreet/SendReport/Angus.pm b/perllib/FixMyStreet/SendReport/Angus.pm
deleted file mode 100644
index 4ba5f3070..000000000
--- a/perllib/FixMyStreet/SendReport/Angus.pm
+++ /dev/null
@@ -1,167 +0,0 @@
-package FixMyStreet::SendReport::Angus;
-
-use Moo;
-
-BEGIN { extends 'FixMyStreet::SendReport'; }
-
-use Try::Tiny;
-use Encode;
-use XML::Simple;
-
-sub get_auth_token {
- my ($self, $authxml) = @_;
-
- my $xml = new XML::Simple;
- my $obj;
-
- eval {
- $obj = $xml->parse_string( $authxml );
- };
-
- my $success = $obj->{success};
- $success =~ s/^\s+|\s+$//g if defined $success;
- my $token = $obj->{AuthenticateResult};
- $token =~ s/^\s+|\s+$//g if defined $token;
-
- if (defined $success && $success eq 'True' && defined $token) {
- return $token;
- } else {
- $self->error("Couldn't authenticate against Angus endpoint.");
- }
-}
-
-sub get_external_id {
- my ($self, $resultxml) = @_;
-
- my $xml = new XML::Simple;
- my $obj;
-
- eval {
- $obj = $xml->parse_string( $resultxml );
- };
-
- my $success = $obj->{success};
- $success =~ s/^\s+|\s+$//g if defined $success;
- my $external_id = $obj->{CreateRequestResult}->{RequestId};
-
- if (defined $success && $success eq 'True' && defined $external_id) {
- return $external_id;
- } else {
- $self->error("Couldn't find external id in response from Angus endpoint.");
- return undef;
- }
-}
-
-sub crm_request_type {
- my ($self, $row, $h) = @_;
- return 'StLight'; # TODO: Set this according to report category
-}
-
-sub jadu_form_fields {
- my ($self, $row, $h) = @_;
- my $xml = XML::Simple->new(
- NoAttr=> 1,
- KeepRoot => 1,
- SuppressEmpty => 0,
- );
- my $metas = $row->get_extra_fields();
- my %extras;
- foreach my $field (@$metas) {
- $extras{$field->{name}} = $field->{value};
- }
- my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new();
- my $output = $xml->XMLout({
- formfields => {
- formfield => [
- {
- name => 'RequestTitle',
- value => $h->{title}
- },
- {
- name => 'RequestDetails',
- value => $h->{detail}
- },
- {
- name => 'ReporterName',
- value => $h->{name}
- },
- {
- name => 'ReporterEmail',
- value => $h->{email}
- },
- {
- name => 'ReporterAnonymity',
- value => $row->anonymous ? 'True' : 'False'
- },
- {
- name => 'ReportedDateTime',
- value => $h->{confirmed}
- },
- {
- name => 'ColumnId',
- value => $extras{'column_id'} || ''
- },
- {
- name => 'ReportId',
- value => $h->{id}
- },
- {
- name => 'ReportedNorthing',
- value => $h->{northing}
- },
- {
- name => 'ReportedEasting',
- value => $h->{easting}
- },
- {
- name => 'Imageurl1',
- value => $row->photos->[0] ? ($cobrand->base_url . $row->photos->[0]->{url_full}) : ''
- },
- {
- name => 'Imageurl2',
- value => $row->photos->[1] ? ($cobrand->base_url . $row->photos->[1]->{url_full}) : ''
- },
- {
- name => 'Imageurl3',
- value => $row->photos->[2] ? ($cobrand->base_url . $row->photos->[2]->{url_full}) : ''
- }
- ]
- }
- });
- # The endpoint crashes if the JADUFormFields string has whitespace between XML elements, so strip it out...
- $output =~ s/>[\s\n]+</></g;
- return $output;
-}
-
-sub send {
- my ( $self, $row, $h ) = @_;
-
- # FIXME: should not recreate this each time
- my $angus_service;
-
- require Integrations::AngusSOAP;
-
- my $return = 1;
- $angus_service ||= Integrations::AngusSOAP->on_fault(sub { my($soap, $res) = @_; die ref $res ? $res->faultstring : $soap->transport->status, "\n"; });
- try {
- my $authresult = $angus_service->AuthenticateJADU();
- my $authtoken = $self->get_auth_token( $authresult );
- # authenticationtoken, CallerId, CallerAddressId, DeliveryId, DeliveryAddressId, CRMRequestType, JADUXFormRef, PaymentRef, JADUFormFields
- my $result = $angus_service->CreateServiceRequest(
- $authtoken, '1', '1', '1', '1', $self->crm_request_type($row, $h),
- 'FMS', '', $self->jadu_form_fields($row, $h)
- );
- my $external_id = $self->get_external_id( $result );
- if ( $external_id ) {
- $row->external_id( $external_id );
- $return = 0;
- }
- } catch {
- my $e = $_;
- $self->error( "Error sending to Angus: $e" );
- };
- $self->success( !$return );
- return $return;
-}
-
-1;
diff --git a/perllib/FixMyStreet/SendReport/Blackhole.pm b/perllib/FixMyStreet/SendReport/Blackhole.pm
new file mode 100644
index 000000000..2c1a4fc8e
--- /dev/null
+++ b/perllib/FixMyStreet/SendReport/Blackhole.pm
@@ -0,0 +1,20 @@
+package FixMyStreet::SendReport::Blackhole;
+
+use Moo;
+
+BEGIN { extends 'FixMyStreet::SendReport'; }
+
+=head2 send
+
+Immediately marks the report as successfully sent, but doesn't actually send
+it anywhere.
+
+=cut
+
+sub send {
+ my $self = shift;
+ $self->success(1);
+ return 0;
+}
+
+1;
diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm
index 583aaaa08..cd697fa0f 100644
--- a/perllib/FixMyStreet/SendReport/Email.pm
+++ b/perllib/FixMyStreet/SendReport/Email.pm
@@ -19,6 +19,9 @@ sub build_recipient_list {
my ($body_email, $state, $note) = ( $contact->email, $contact->state, $contact->note );
+ $body_email = swandt_contact($row->latitude, $row->longitude)
+ if ($body->areas->{2427} || $body->areas->{2429}) && $body_email eq 'SPECIAL';
+
unless ($state eq 'confirmed') {
$all_confirmed = 0;
$note = 'Body ' . $row->bodies_str . ' deleted'
@@ -57,7 +60,7 @@ sub send {
my $self = shift;
my ( $row, $h ) = @_;
- my $recips = $self->build_recipient_list( $row, $h );
+ my $recips = @{$self->to} ? 1 : $self->build_recipient_list( $row, $h );
# on a staging server send emails to ourselves rather than the bodies
if (FixMyStreet->staging_flag('send_reports', 0) && !FixMyStreet->test_mode) {
@@ -71,7 +74,9 @@ sub send {
}
my ($verbose, $nomail) = CronFns::options();
- my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new();
+ my $cobrand = $row->get_cobrand_logged;
+ $cobrand = $cobrand->call_hook(get_body_handler_for_problem => $row) || $cobrand;
+
my $params = {
To => $self->to,
};
@@ -109,10 +114,19 @@ sub send {
return $result;
}
+# SW&T has different contact addresses depending upon the old district
+sub swandt_contact {
+ my $district = _get_district_for_contact(@_);
+ my $email;
+ $email = ['customerservices', 'westsomerset'] if $district == 2427;
+ $email = ['enquiries', 'tauntondeane'] if $district == 2429;
+ return join('@', $email->[0], $email->[1] . '.gov.uk');
+}
+
sub _get_district_for_contact {
my ( $lat, $lon ) = @_;
my $district =
- mySociety::MaPit::call( 'point', "4326/$lon,$lat", type => 'DIS' );
+ FixMyStreet::MapIt::call( 'point', "4326/$lon,$lat", type => 'DIS' );
($district) = keys %$district;
return $district;
}
diff --git a/perllib/FixMyStreet/SendReport/Email/Highways.pm b/perllib/FixMyStreet/SendReport/Email/Highways.pm
new file mode 100644
index 000000000..2a1f7b305
--- /dev/null
+++ b/perllib/FixMyStreet/SendReport/Email/Highways.pm
@@ -0,0 +1,11 @@
+package FixMyStreet::SendReport::Email::Highways;
+
+use Moo;
+extends 'FixMyStreet::SendReport::Email::SingleBodyOnly';
+
+has contact => (
+ is => 'ro',
+ default => 'Pothole'
+);
+
+1;
diff --git a/perllib/FixMyStreet/SendReport/Email/SingleBodyOnly.pm b/perllib/FixMyStreet/SendReport/Email/SingleBodyOnly.pm
new file mode 100644
index 000000000..cf778c549
--- /dev/null
+++ b/perllib/FixMyStreet/SendReport/Email/SingleBodyOnly.pm
@@ -0,0 +1,28 @@
+package FixMyStreet::SendReport::Email::SingleBodyOnly;
+
+use Moo;
+extends 'FixMyStreet::SendReport::Email';
+
+has contact => (
+ is => 'ro',
+ default => sub { die 'Need to override contact' }
+);
+
+sub build_recipient_list {
+ my ( $self, $row, $h ) = @_;
+
+ return unless @{$self->bodies} == 1;
+ my $body = $self->bodies->[0];
+
+ # We don't care what the category was, look up the relevant contact
+ my $contact = $row->result_source->schema->resultset("Contact")->not_deleted->find({
+ body_id => $body->id,
+ category => $self->contact,
+ });
+ return unless $contact;
+
+ @{$self->to} = map { [ $_, $body->name ] } split /,/, $contact->email;
+ return 1;
+}
+
+1;
diff --git a/perllib/FixMyStreet/SendReport/Email/TfL.pm b/perllib/FixMyStreet/SendReport/Email/TfL.pm
new file mode 100644
index 000000000..383df9792
--- /dev/null
+++ b/perllib/FixMyStreet/SendReport/Email/TfL.pm
@@ -0,0 +1,11 @@
+package FixMyStreet::SendReport::Email::TfL;
+
+use Moo;
+extends 'FixMyStreet::SendReport::Email::SingleBodyOnly';
+
+has contact => (
+ is => 'ro',
+ default => 'Traffic lights'
+);
+
+1;
diff --git a/perllib/FixMyStreet/SendReport/Open311.pm b/perllib/FixMyStreet/SendReport/Open311.pm
index 84aa851ed..a661ff206 100644
--- a/perllib/FixMyStreet/SendReport/Open311.pm
+++ b/perllib/FixMyStreet/SendReport/Open311.pm
@@ -29,6 +29,7 @@ sub send {
use_service_as_deviceid => 0,
extended_description => 1,
multi_photos => 0,
+ fixmystreet_body => $body,
);
my $cobrand = $body->get_cobrand_handler || $row->get_cobrand_logged;
@@ -94,6 +95,8 @@ sub send {
$self->error( "Failed to send over Open311\n" ) unless $self->error;
$self->error( $self->error . "\n" . $open311->error );
}
+
+ $cobrand->call_hook(open311_post_send => $row, $h);
}
diff --git a/perllib/FixMyStreet/Template.pm b/perllib/FixMyStreet/Template.pm
index 4a9cffecb..9c565114b 100644
--- a/perllib/FixMyStreet/Template.pm
+++ b/perllib/FixMyStreet/Template.pm
@@ -114,7 +114,7 @@ into <br>s too.
sub html_paragraph : Filter('html_para') {
my $text = shift;
my @paras = split(/(?:\r?\n){2,}/, $text);
- s/\r?\n/<br>\n/ for @paras;
+ s/\r?\n/<br>\n/g for @paras;
$text = "<p>\n" . join("\n</p>\n\n<p>\n", @paras) . "</p>\n";
return $text;
}
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index c5b72a7cf..3ecb13b6a 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -10,6 +10,7 @@ sub import {
Test::More->export_to_level(1);
}
+use Encode;
use Test::WWW::Mechanize::Catalyst 'FixMyStreet::App';
use t::Mock::MapIt;
use Test::More;
@@ -430,7 +431,7 @@ sub extract_problem_title {
$banner = $mech->extract_problem_banner;
-Returns the problem title from a problem report page. Returns a hashref with id and text.
+Returns the problem title from a problem report page. Returns a hashref with class and text.
=cut
@@ -438,8 +439,8 @@ sub extract_problem_banner {
my $mech = shift;
my $result = scraper {
- process 'div#side > p.banner', id => '@id', text => 'TEXT';
- process 'div.banner > p', id => '@id', text => 'TEXT';
+ process 'div.banner', class => '@class';
+ process 'div.banner > p', text => 'TEXT';
}
->scrape( $mech->response );
@@ -536,31 +537,6 @@ sub visible_form_values {
return \%params;
}
-=head2 session_cookie_expiry
-
- $expiry = $mech->session_cookie_expiry( );
-
-Returns the current expiry time for the session cookie. Might be '0' which
-indicates it expires at end of browser session.
-
-=cut
-
-sub session_cookie_expiry {
- my $mech = shift;
-
- my $cookie_name = 'fixmystreet_app_session';
- my $expires = 'not found';
-
- $mech #
- ->cookie_jar #
- ->scan( sub { $expires = $_[8] if $_[1] eq $cookie_name } );
-
- croak "Could not find cookie '$cookie_name'"
- if $expires && $expires eq 'not found';
-
- return $expires || 0;
-}
-
=head2 get_ok_json
$decoded = $mech->get_ok_json( $url );
@@ -705,7 +681,7 @@ sub create_problems_for_body {
latitude => '51.5016605453401',
longitude => '-0.142497580865087',
user_id => $user->id,
- photo => $mech->get_photo_data,
+ photo => '74e3362283b6ef0c48686fb0e161da4043bbcc97.jpeg',
};
my %report_params = ( %$default_params, %$params );
@@ -720,15 +696,6 @@ sub create_problems_for_body {
return @problems;
}
-sub get_photo_data {
- my $mech = shift;
- return $mech->{sample_photo} ||= do {
- my $sample_file = FixMyStreet->path_to( 't/app/controller/sample.jpg' );
- $mech->builder->ok( -f "$sample_file", "sample file $sample_file exists" );
- $sample_file->slurp(iomode => '<:raw');
- };
-}
-
sub create_comment_for_problem {
my ( $mech, $problem, $user, $name, $text, $anonymous, $state, $problem_state, $params ) = @_;
$params ||= {};
@@ -743,4 +710,21 @@ sub create_comment_for_problem {
FixMyStreet::App->model('DB::Comment')->create($params);
}
+
+sub encoded_content {
+ my $self = shift;
+ return encode_utf8($self->content);
+}
+
+sub content_as_csv {
+ my $self = shift;
+ open my $data_handle, '<', \$self->content;
+ my $csv = Text::CSV->new({ binary => 1 });
+ my @rows;
+ while (my $row = $csv->getline($data_handle)) {
+ push @rows, $row;
+ }
+ return @rows;
+}
+
1;