aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App.pm25
-rwxr-xr-xperllib/FixMyStreet/App/Controller/About.pm14
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm269
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm8
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm199
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm55
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm7
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/States.pm96
-rw-r--r--perllib/FixMyStreet/App/Controller/Around.pm129
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm22
-rw-r--r--perllib/FixMyStreet/App/Controller/Contact.pm28
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm22
-rw-r--r--perllib/FixMyStreet/App/Controller/JSON.pm7
-rw-r--r--perllib/FixMyStreet/App/Controller/Location.pm2
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm29
-rw-r--r--perllib/FixMyStreet/App/Controller/My.pm51
-rw-r--r--perllib/FixMyStreet/App/Controller/Open311.pm9
-rw-r--r--perllib/FixMyStreet/App/Controller/Report.pm164
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm85
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm43
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm96
-rw-r--r--perllib/FixMyStreet/App/Controller/Root.pm38
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Rss.pm26
-rw-r--r--perllib/FixMyStreet/App/Model/DB.pm21
-rw-r--r--perllib/FixMyStreet/App/View/Web.pm7
-rw-r--r--perllib/FixMyStreet/Cobrand.pm4
-rw-r--r--perllib/FixMyStreet/Cobrand/Angus.pm8
-rw-r--r--perllib/FixMyStreet/Cobrand/Borsetshire.pm32
-rw-r--r--perllib/FixMyStreet/Cobrand/Bristol.pm4
-rw-r--r--perllib/FixMyStreet/Cobrand/Bromley.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm117
-rw-r--r--perllib/FixMyStreet/Cobrand/EastHerts.pm4
-rw-r--r--perllib/FixMyStreet/Cobrand/FiksGataMi.pm4
-rw-r--r--perllib/FixMyStreet/Cobrand/FixMyStreet.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/FixaMinGata.pm4
-rw-r--r--perllib/FixMyStreet/Cobrand/Greenwich.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Hart.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Oxfordshire.pm33
-rw-r--r--perllib/FixMyStreet/Cobrand/Stevenage.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/UK.pm50
-rw-r--r--perllib/FixMyStreet/Cobrand/UKCouncils.pm32
-rw-r--r--perllib/FixMyStreet/Cobrand/Warwickshire.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/WestBerkshire.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Whitelabel.pm13
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm11
-rw-r--r--perllib/FixMyStreet/DB.pm17
-rw-r--r--perllib/FixMyStreet/DB/Factories.pm173
-rw-r--r--perllib/FixMyStreet/DB/Result/Body.pm34
-rw-r--r--perllib/FixMyStreet/DB/Result/Comment.pm113
-rw-r--r--perllib/FixMyStreet/DB/Result/Contact.pm36
-rw-r--r--perllib/FixMyStreet/DB/Result/ContactsHistory.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm201
-rw-r--r--perllib/FixMyStreet/DB/Result/ReportExtraFields.pm45
-rw-r--r--perllib/FixMyStreet/DB/Result/ResponsePriority.pm6
-rw-r--r--perllib/FixMyStreet/DB/Result/State.pm48
-rw-r--r--perllib/FixMyStreet/DB/Result/Translation.pm44
-rw-r--r--perllib/FixMyStreet/DB/Result/User.pm43
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Body.pm11
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Contact.pm15
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/DefectType.pm32
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm25
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm24
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm27
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/State.pm84
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm12
-rw-r--r--perllib/FixMyStreet/DB/Schema.pm32
-rw-r--r--perllib/FixMyStreet/Email/Sender.pm5
-rw-r--r--perllib/FixMyStreet/Integrations/ExorRDI.pm243
-rw-r--r--perllib/FixMyStreet/Roles/ContactExtra.pm48
-rw-r--r--perllib/FixMyStreet/Roles/Extra.pm16
-rw-r--r--perllib/FixMyStreet/Roles/Translatable.pm116
-rw-r--r--perllib/FixMyStreet/Script/Alerts.pm15
-rw-r--r--perllib/FixMyStreet/Script/Questionnaires.pm3
-rw-r--r--perllib/FixMyStreet/Script/Reports.pm2
-rwxr-xr-xperllib/FixMyStreet/Script/UpdateAllReports.pm274
-rw-r--r--perllib/FixMyStreet/SendReport/Angus.pm1
-rw-r--r--perllib/FixMyStreet/SendReport/EastHants.pm6
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm7
-rw-r--r--perllib/FixMyStreet/SendReport/Open311.pm3
-rw-r--r--perllib/FixMyStreet/Test.pm32
-rw-r--r--perllib/FixMyStreet/TestAppProve.pm24
-rw-r--r--perllib/FixMyStreet/TestMech.pm53
82 files changed, 2677 insertions, 985 deletions
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm
index 35e8c2537..a0477ca40 100644
--- a/perllib/FixMyStreet/App.pm
+++ b/perllib/FixMyStreet/App.pm
@@ -25,7 +25,6 @@ use Catalyst (
'Session::State::Cookie', # FIXME - we're using our own override atm
'Authentication',
'SmartURI',
- 'Compress::Gzip',
);
extends 'Catalyst';
@@ -168,9 +167,13 @@ template paths, maps, languages etc, etc.
sub setup_request {
my $c = shift;
+ # Set the Catalyst model schema to the same as the DB schema
+ $c->model("DB")->schema( FixMyStreet::DB->schema );
+
$c->setup_dev_overrides();
my $cobrand = $c->cobrand;
+ FixMyStreet::DB->schema->cobrand($cobrand);
$cobrand->call_hook('add_response_headers');
@@ -200,6 +203,10 @@ sub setup_request {
$c->stash->{site_name} = Utils::trim_text($c->render_fragment('site-name.html'));
+ if (my $template = $c->forward('/about/find_template', [ 'homepage' ])) {
+ $c->stash->{homepage_template} = $template;
+ }
+
$c->model('DB::Problem')->set_restriction( $cobrand->site_key() );
Memcached::set_namespace( FixMyStreet->config('FMS_DB_NAME') . ":" );
@@ -212,11 +219,8 @@ sub setup_request {
mySociety::MaPit::configure( "http://$host/fakemapit/" );
}
- # XXX Put in cobrand / do properly
- if ($c->cobrand->moniker eq 'zurich') {
- FixMyStreet::DB::Result::Problem->visible_states_add('unconfirmed');
- FixMyStreet::DB::Result::Problem->visible_states_remove('investigating');
- }
+ $c->stash->{has_fixed_state} = FixMyStreet::DB::Result::Problem::fixed_states->{fixed};
+ $c->cobrand->call_hook('setup_states');
if (FixMyStreet->test_mode) {
# Is there a better way of altering $c->config that may have
@@ -293,7 +297,7 @@ sub get_override {
=head2 send_email
- $email_sent = $c->send_email( 'email_template.txt', $extra_stash_values );
+ $success = $c->send_email( 'email_template.txt', $extra_stash_values );
Send an email by filling in the given template with values in the stash.
@@ -305,6 +309,8 @@ set those fields in the email if they are present.
If a 'from' is not specified then the default from the config is used.
+Returns the email on success, false on failure.
+
=cut
sub send_email {
@@ -349,14 +355,15 @@ sub send_email {
my $email = mySociety::Locale::in_gb_locale { FixMyStreet::Email::construct_email($data) };
+ my $result = 0;
try {
FixMyStreet::Email::Sender->send($email, { from => $sender });
+ $result = $email;
} catch {
my $error = $_ || 'unknown error';
$c->log->error("$error");
};
-
- return $email;
+ return $result;
}
=head2 uri_with
diff --git a/perllib/FixMyStreet/App/Controller/About.pm b/perllib/FixMyStreet/App/Controller/About.pm
index 78e548c5f..233da25d3 100755
--- a/perllib/FixMyStreet/App/Controller/About.pm
+++ b/perllib/FixMyStreet/App/Controller/About.pm
@@ -34,22 +34,24 @@ sub index : Path("/about") : Args(0) {
sub find_template : Private {
my ( $self, $c, $page ) = @_;
- return $found{$page} if !FixMyStreet->config('STAGING_SITE') && exists $found{$page};
-
my $lang_code = $c->stash->{lang_code};
+
+ return $found{$lang_code}{$page} if !FixMyStreet->config('STAGING_SITE') &&
+ exists $found{$lang_code}{$page};
+
foreach my $dir_templates (@{$c->stash->{additional_template_paths}}, @{$c->view('Web')->paths}) {
foreach my $dir_static (static_dirs($page, $dir_templates)) {
foreach my $file ("$page-$lang_code.html", "$page.html") {
if (-e "$dir_templates/$dir_static/$file") {
- $found{$page} = "$dir_static/$file";
- return $found{$page};
+ $found{$lang_code}{$page} = "$dir_static/$file";
+ return $found{$lang_code}{$page};
}
}
}
}
# Cache that the page does not exist, so we don't look next time
- $found{$page} = undef;
- return $found{$page};
+ $found{$lang_code}{$page} = undef;
+ return $found{$lang_code}{$page};
}
sub static_dirs {
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index 1f3307710..ed40f4565 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -8,9 +8,10 @@ use Path::Class;
use POSIX qw(strftime strcoll);
use Digest::SHA qw(sha1_hex);
use mySociety::EmailUtil qw(is_valid_email is_valid_email_list);
-use mySociety::ArrayUtils;
use DateTime::Format::Strptime;
use List::Util 'first';
+use List::MoreUtils 'uniq';
+use mySociety::ArrayUtils;
use FixMyStreet::SendReport;
@@ -26,7 +27,7 @@ Admin pages
=cut
-sub begin : Private {
+sub auto : Private {
my ( $self, $c ) = @_;
$c->uri_disposition('relative');
@@ -43,10 +44,6 @@ sub begin : Private {
if ( $c->cobrand->moniker eq 'zurich' ) {
$c->cobrand->admin_type();
}
-}
-
-sub auto : Private {
- my ( $self, $c ) = @_;
$c->forward('check_page_allowed');
}
@@ -97,11 +94,11 @@ sub index : Path : Args(0) {
my $contacts = $c->model('DB::Contact')->summary_count();
my %contact_counts =
- map { $_->confirmed => $_->get_column('confirmed_count') } $contacts->all;
+ map { $_->state => $_->get_column('state_count') } $contacts->all;
- $contact_counts{0} ||= 0;
- $contact_counts{1} ||= 0;
- $contact_counts{total} = $contact_counts{0} + $contact_counts{1};
+ $contact_counts{confirmed} ||= 0;
+ $contact_counts{unconfirmed} ||= 0;
+ $contact_counts{total} = $contact_counts{confirmed} + $contact_counts{unconfirmed};
$c->stash->{contacts} = \%contact_counts;
@@ -243,6 +240,9 @@ sub bodies : Path('bodies') : Args(0) {
$c->stash->{edit_activity} = $edit_activity;
+ $c->forward( 'fetch_languages' );
+ $c->forward( 'fetch_translations' );
+
my $posted = $c->get_param('posted') || '';
if ( $posted eq 'body' ) {
$c->forward('check_for_super_user');
@@ -256,6 +256,9 @@ sub bodies : Path('bodies') : Args(0) {
$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');
}
}
@@ -265,8 +268,8 @@ sub bodies : Path('bodies') : Args(0) {
my $contacts = $c->model('DB::Contact')->search(
undef,
{
- select => [ 'body_id', { count => 'id' }, { count => \'case when deleted then 1 else null end' },
- { count => \'case when confirmed then 1 else null end' } ],
+ 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'
@@ -299,30 +302,6 @@ sub body_form_dropdowns : Private {
$c->stash->{send_methods} = \@methods;
}
-sub body : Path('body') : Args(1) {
- my ( $self, $c, $body_id ) = @_;
-
- $c->stash->{body_id} = $body_id;
-
- unless ($c->user->has_permission_to('category_edit', $body_id)) {
- $c->forward('check_for_super_user');
- }
-
- $c->forward( '/auth/get_csrf_token' );
- $c->forward( 'lookup_body' );
- $c->forward( 'fetch_all_bodies' );
- $c->forward( 'body_form_dropdowns' );
-
- if ( $c->get_param('posted') ) {
- $c->log->debug( 'posted' );
- $c->forward('update_contacts');
- }
-
- $c->forward('fetch_contacts');
-
- return 1;
-}
-
sub check_for_super_user : Private {
my ( $self, $c ) = @_;
@@ -365,8 +344,7 @@ sub update_contacts : Private {
}
$contact->email( $email );
- $contact->confirmed( $c->get_param('confirmed') ? 1 : 0 );
- $contact->deleted( $c->get_param('deleted') ? 1 : 0 );
+ $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' );
@@ -393,6 +371,8 @@ sub update_contacts : Private {
$contact->set_extra_metadata( reputation_threshold => int($c->get_param('reputation_threshold')) );
}
+ $c->forward('update_extra_fields', [ $contact ]);
+
if ( %errors ) {
$c->stash->{updated} = _('Please correct the errors below');
$c->stash->{contact} = $contact;
@@ -407,6 +387,12 @@ sub update_contacts : Private {
$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');
@@ -421,7 +407,7 @@ sub update_contacts : Private {
$contacts->update(
{
- confirmed => 1,
+ state => 'confirmed',
whenedited => \'current_timestamp',
note => 'Confirmed',
editor => $editor,
@@ -446,11 +432,43 @@ sub update_contacts : Private {
# 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 ) = @_;
@@ -485,8 +503,8 @@ sub fetch_contacts : Private {
my $contacts = $c->stash->{body}->contacts->search(undef, { order_by => [ 'category' ] } );
$c->stash->{contacts} = $contacts;
- $c->stash->{live_contacts} = $contacts->search({ deleted => 0 });
- $c->stash->{any_not_confirmed} = $contacts->search({ confirmed => 0 })->count;
+ $c->stash->{live_contacts} = $contacts->search({ state => { '!=' => 'deleted' } });
+ $c->stash->{any_not_confirmed} = $contacts->search({ state => 'unconfirmed' })->count;
if ( $c->get_param('text') && $c->get_param('text') eq '1' ) {
$c->stash->{template} = 'admin/council_contacts.txt';
@@ -497,10 +515,48 @@ sub fetch_contacts : Private {
return 1;
}
-sub lookup_body : Private {
+sub fetch_languages : Private {
my ( $self, $c ) = @_;
- my $body_id = $c->stash->{body_id};
+ my $lang_map = {};
+ foreach my $lang (@{$c->cobrand->languages}) {
+ my ($id, $name, $code) = split(',', $lang);
+ $lang_map->{$id} = { name => $name, code => $code };
+ }
+
+ $c->stash->{languages} = $lang_map;
+
+ 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 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;
@@ -512,39 +568,70 @@ sub lookup_body : Private {
$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');
+ $c->forward('fetch_contacts');
+
+ $c->stash->{template} = 'admin/body.html';
return 1;
}
-# This is for if the category name contains a '/'
-sub category_edit_all : Path('body') {
- my ( $self, $c, $body_id, @category ) = @_;
+sub category : Chained('body') : PathPart('') {
+ my ( $self, $c, @category ) = @_;
my $category = join( '/', @category );
- $c->go( 'category_edit', [ $body_id, $category ] );
-}
-
-sub category_edit : Path('body') : Args(2) {
- my ( $self, $c, $body_id, $category ) = @_;
-
- $c->stash->{body_id} = $body_id;
$c->forward( '/auth/get_csrf_token' );
- $c->forward( 'lookup_body' );
+ $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 => $body_id,
- category => $category
+ 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:://; $_ } keys %{ FixMyStreet::SendReport->get_senders };
$c->stash->{send_methods} = \@methods;
@@ -844,10 +931,26 @@ sub report_edit_category : Private {
$problem->category($category);
my @contacts = grep { $_->category eq $problem->category } @{$c->stash->{contacts}};
my @new_body_ids = map { $_->body_id } @contacts;
- # If the report has changed bodies we need to resend it
- if (scalar @{mySociety::ArrayUtils::symmetric_diff($problem->bodies_str_ids, \@new_body_ids)}) {
+ # 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);
+ }
+ # If the send methods of the old/new contacts differ we need to resend the report
+ my @old_contacts = grep { $_->category eq $category_old } @{$c->stash->{contacts}};
+ my @new_send_methods = uniq map {
+ ( $_->body->can_be_devolved && $_->send_method ) ?
+ $_->send_method : $_->body->send_method;
+ } @contacts;
+ my @old_send_methods = map {
+ ( $_->body->can_be_devolved && $_->send_method ) ?
+ $_->send_method : $_->body->send_method;
+ } @old_contacts;
+ if ( scalar @{ mySociety::ArrayUtils::symmetric_diff(\@old_send_methods, \@new_send_methods) } ) {
+ $c->log->debug("Report changed, resending");
$problem->whensent(undef);
}
+
$problem->bodies_str(join( ',', @new_body_ids ));
$problem->add_to_comments({
text => '*' . sprintf(_('Category changed from ‘%s’ to ‘%s’'), $category_old, $category) . '*',
@@ -909,8 +1012,8 @@ sub categories_for_point : Private {
# Remove the "Pick a category" option
shift @{$c->stash->{category_options}} if @{$c->stash->{category_options}};
- $c->stash->{categories} = $c->stash->{category_options};
- $c->stash->{categories_hash} = { map { $_ => 1 } @{$c->stash->{category_options}} };
+ $c->stash->{category_options_copy} = $c->stash->{category_options};
+ $c->stash->{categories_hash} = { map { $_->{name} => 1 } @{$c->stash->{category_options}} };
}
sub templates : Path('templates') : Args(0) {
@@ -966,7 +1069,7 @@ sub template_edit : Path('templates') : Args(2) {
my %active_contacts = map { $_->id => 1 } @contacts;
my @all_contacts = map { {
id => $_->id,
- category => $_->category,
+ category => $_->category_display,
active => $active_contacts{$_->id},
} } @live_contacts;
$c->stash->{contacts} = \@all_contacts;
@@ -1808,7 +1911,7 @@ sub check_page_allowed : Private {
sub fetch_all_bodies : Private {
my ($self, $c ) = @_;
- my @bodies = $c->model('DB::Body')->all;
+ my @bodies = $c->model('DB::Body')->all_translated;
if ( $c->cobrand->moniker eq 'zurich' ) {
@bodies = $c->cobrand->admin_fetch_all_bodies( @bodies );
} else {
@@ -1840,6 +1943,46 @@ sub fetch_body_areas : Private {
$c->stash->{fetched_areas_body_id} = $body->id;
}
+sub update_extra_fields : Private {
+ my ($self, $c, $object) = @_;
+
+ my @indices = grep { /^metadata\[\d+\]\.code/ } keys %{ $c->req->params };
+ @indices = sort map { /(\d+)/ } @indices;
+
+ my @extra_fields;
+ foreach my $i (@indices) {
+ my $meta = {};
+ $meta->{code} = $c->get_param("metadata[$i].code");
+ next unless $meta->{code};
+ $meta->{order} = int $c->get_param("metadata[$i].order");
+ $meta->{datatype} = $c->get_param("metadata[$i].datatype");
+ my $required = $c->get_param("metadata[$i].required") && $c->get_param("metadata[$i].required") eq 'on';
+ $meta->{required} = $required ? 'true' : 'false';
+ my $notice = $c->get_param("metadata[$i].notice") && $c->get_param("metadata[$i].notice") eq 'on';
+ $meta->{variable} = $notice ? 'false' : 'true';
+ $meta->{description} = $c->get_param("metadata[$i].description");
+ $meta->{datatype_description} = $c->get_param("metadata[$i].datatype_description");
+
+ if ( $meta->{datatype} eq "singlevaluelist" ) {
+ $meta->{values} = [];
+ my $re = qr{^metadata\[$i\]\.values\[\d+\]\.key};
+ my @vindices = grep { /$re/ } keys %{ $c->req->params };
+ @vindices = sort map { /values\[(\d+)\]/ } @vindices;
+ foreach my $j (@vindices) {
+ my $name = $c->get_param("metadata[$i].values[$j].name");
+ my $key = $c->get_param("metadata[$i].values[$j].key");
+ push(@{$meta->{values}}, {
+ name => $name,
+ key => $key,
+ }) if $name;
+ }
+ }
+ push @extra_fields, $meta;
+ }
+ @extra_fields = sort { $a->{order} <=> $b->{order} } @extra_fields;
+ $object->set_extra_fields(@extra_fields);
+}
+
sub trim {
my $self = shift;
my $e = shift;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
index bcfeb3dd8..5dab1da2c 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
@@ -6,12 +6,6 @@ use mySociety::ArrayUtils;
BEGIN { extends 'Catalyst::Controller'; }
-sub begin : Private {
- my ( $self, $c ) = @_;
-
- $c->forward('/admin/begin');
-}
-
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
@@ -62,7 +56,7 @@ sub edit : Path : Args(2) {
my %active_contacts = map { $_->id => 1 } @contacts;
my @all_contacts = map { {
id => $_->id,
- category => $_->category,
+ category => $_->category_display,
active => $active_contacts{$_->id},
} } @live_contacts;
$c->stash->{contacts} = \@all_contacts;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
index 201742c81..bdeecc1a3 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
@@ -2,19 +2,13 @@ package FixMyStreet::App::Controller::Admin::ExorDefects;
use Moose;
use namespace::autoclean;
-use Text::CSV;
use DateTime;
-use mySociety::Random qw(random_bytes);
+use Try::Tiny;
+use FixMyStreet::Integrations::ExorRDI;
BEGIN { extends 'Catalyst::Controller'; }
-sub begin : Private {
- my ( $self, $c ) = @_;
-
- $c->forward('/admin/begin');
-}
-
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
@@ -54,177 +48,32 @@ sub download : Path('download') : Args(0) {
my $end_date = $parser-> parse_datetime ( $c->get_param('end_date') ) ;
my $one_day = DateTime::Duration->new( days => 1 );
- my %params = (
- -and => [
- state => [ 'action scheduled' ],
- external_id => { '!=' => undef },
- 'admin_log_entries.action' => 'inspected',
- 'admin_log_entries.whenedited' => { '>=', $start_date },
- 'admin_log_entries.whenedited' => { '<=', $end_date + $one_day },
- ]
- );
-
- my $user;
- if ( $c->get_param('user_id') ) {
- my $uid = $c->get_param('user_id');
- $params{'admin_log_entries.user_id'} = $uid;
- $user = $c->model('DB::User')->find( { id => $uid } );
- }
-
- my $problems = $c->cobrand->problems->search(
- \%params,
- {
- join => 'admin_log_entries',
- distinct => 1,
- }
- );
-
- if ( !$problems->count ) {
- if ( defined $user ) {
+ my $params = {
+ start_date => $start_date,
+ inspection_date => $start_date,
+ end_date => $end_date + $one_day,
+ user => $c->get_param('user_id'),
+ mark_as_processed => 0,
+ };
+ my $rdi = FixMyStreet::Integrations::ExorRDI->new($params);
+
+ try {
+ my $out = $rdi->construct;
+ $c->res->content_type('text/csv; charset=utf-8');
+ $c->res->header('content-disposition' => "attachment; filename=" . $rdi->filename);
+ $c->res->body( $out );
+ } catch {
+ die $_ unless $_ =~ /FixMyStreet::Integrations::ExorRDI::Error/;
+ if ($params->{user}) {
$c->flash->{error_message} = _("No inspections by that inspector in the selected date range.");
} else {
$c->flash->{error_message} = _("No inspections in the selected date range.");
}
- $c->flash->{start_date} = $start_date;
- $c->flash->{end_date} = $end_date;
- $c->flash->{user_id} = $user->id if $user;
+ $c->flash->{start_date} = $params->{start_date};
+ $c->flash->{end_date} = $params->{end_date};
+ $c->flash->{user_id} = $params->{user};
$c->res->redirect( $c->uri_for( '' ) );
- }
-
- # A single RDI file might contain inspections from multiple inspectors, so
- # we need to group inspections by inspector within G records.
- my $inspectors = {};
- my $inspector_initials = {};
- while ( my $report = $problems->next ) {
- my $user = $report->inspection_log_entry->user;
- $inspectors->{$user->id} ||= [];
- push @{ $inspectors->{$user->id} }, $report;
- unless ( $inspector_initials->{$user->id} ) {
- $inspector_initials->{$user->id} = $user->get_extra_metadata('initials');
- }
- }
-
- my $csv = Text::CSV->new({ binary => 1, eol => "" });
-
- my $p_count = 0;
- my $link_id = $c->cobrand->exor_rdi_link_id;
-
- # RDI first line is always the same
- $csv->combine("1", "1.8", "1.0.0.0", "ENHN", "");
- my @body = ($csv->string);
-
- my $i = 0;
- foreach my $inspector_id (keys %$inspectors) {
- my $inspections = $inspectors->{$inspector_id};
- my $initials = $inspector_initials->{$inspector_id};
-
- $csv->combine(
- "G", # start of an area/sequence
- $link_id, # area/link id, fixed value for our purposes
- "","", # must be empty
- $initials || "XX", # inspector initials
- $start_date->strftime("%y%m%d"), # date of inspection yymmdd
- "0700", # time of inspection hhmm, set to static value for now
- "D", # inspection variant, should always be D
- "INS", # inspection type, always INS
- "N", # Area of the county - north (N) or south (S)
- "", "", "", "" # empty fields
- );
- push @body, $csv->string;
-
- $csv->combine(
- "H", # initial inspection type
- "MC" # minor carriageway (changes depending on activity code)
- );
- push @body, $csv->string;
-
- foreach my $report (@$inspections) {
- my ($eastings, $northings) = $report->local_coords;
- my $description = sprintf("%s %s", $report->external_id || "", $report->get_extra_metadata('detailed_information') || "");
- my $activity_code = $report->defect_type ?
- $report->defect_type->get_extra_metadata('activity_code')
- : 'MC';
- my $traffic_information = $report->get_extra_metadata('traffic_information') ?
- 'TM ' . $report->get_extra_metadata('traffic_information')
- : 'TM none';
-
- $csv->combine(
- "I", # beginning of defect record
- $activity_code, # activity code - minor carriageway, also FC (footway)
- "", # empty field, can also be A (seen on MC) or B (seen on FC)
- sprintf("%03d", ++$i), # randomised sequence number
- "${eastings}E ${northings}N", # defect location field, which we don't capture from inspectors
- $report->inspection_log_entry->whenedited->strftime("%H%M"), # defect time raised
- "","","","","","","", # empty fields
- $traffic_information,
- $description, # defect description
- );
- push @body, $csv->string;
-
- my $defect_type = $report->defect_type ?
- $report->defect_type->get_extra_metadata('defect_code')
- : 'SFP2';
- $csv->combine(
- "J", # georeferencing record
- $defect_type, # defect type - SFP2: sweep and fill <1m2, POT2 also seen
- $report->response_priority ?
- $report->response_priority->external_id :
- "2", # priority of defect
- "","", # empty fields
- $eastings, # eastings
- $northings, # northings
- "","","","","" # empty fields
- );
- push @body, $csv->string;
-
- $csv->combine(
- "M", # bill of quantities record
- "resolve", # permanent repair
- "","", # empty fields
- "/CMC", # /C + activity code
- "", "" # empty fields
- );
- push @body, $csv->string;
- }
-
- # end this group of defects with a P record
- $csv->combine(
- "P", # end of area/sequence
- 0, # always 0
- 999999, # charging code, always 999999 in OCC
- );
- push @body, $csv->string;
- $p_count++;
- }
-
- # end the RDI file with an X record
- my $record_count = $i;
- $csv->combine(
- "X", # end of inspection record
- $p_count,
- $p_count,
- $record_count, # number of I records
- $record_count, # number of J records
- 0, 0, 0, # always zero
- $record_count, # number of M records
- 0, # always zero
- $p_count,
- 0, 0, 0 # error counts, always zero
- );
- push @body, $csv->string;
-
- my $start = $start_date->strftime("%Y%m%d");
- my $end = $end_date->strftime("%Y%m%d");
- my $filename = sprintf("exor_defects-%s-%s.rdi", $start, $end);
- if ( $user ) {
- my $initials = $user->get_extra_metadata("initials") || "";
- $filename = sprintf("exor_defects-%s-%s-%s.rdi", $start, $end, $initials);
- }
- $c->res->content_type('text/csv; charset=utf-8');
- $c->res->header('content-disposition' => "attachment; filename=$filename");
- # The RDI format is very weird CSV - each line must be wrapped in
- # double quotes.
- $c->res->body( join "", map { "\"$_\"\r\n" } @body );
+ };
}
-1; \ No newline at end of file
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm b/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm
new file mode 100644
index 000000000..337fb4bed
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm
@@ -0,0 +1,55 @@
+package FixMyStreet::App::Controller::Admin::ReportExtraFields;
+use Moose;
+use namespace::autoclean;
+use List::MoreUtils qw(uniq);
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my @extras = $c->model('DB::ReportExtraFields')->search(
+ undef,
+ {
+ order_by => 'name'
+ }
+ );
+
+ $c->stash->{extra_fields} = \@extras;
+}
+
+sub edit : Path : Args(1) {
+ my ( $self, $c, $extra_id ) = @_;
+
+ my $extra;
+ if ( $extra_id eq 'new' ) {
+ $extra = $c->model('DB::ReportExtraFields')->new({});
+ } else {
+ $extra = $c->model('DB::ReportExtraFields')->find( $extra_id )
+ or $c->detach( '/page_error_404_not_found' );
+ }
+
+ if ($c->req->method eq 'POST') {
+ $c->forward('/auth/check_csrf_token');
+
+ foreach (qw/name cobrand language/) {
+ $extra->$_($c->get_param($_));
+ }
+ $c->forward('/admin/update_extra_fields', [ $extra ]);
+
+ $extra->update_or_insert;
+ }
+
+ $c->forward('/auth/get_csrf_token');
+ $c->forward('/admin/fetch_languages');
+
+ my @cobrands = uniq sort map { $_->{moniker} } FixMyStreet::Cobrand->available_cobrand_classes;
+ $c->stash->{cobrands} = \@cobrands;
+
+ $c->stash->{extra} = $extra;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm b/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm
index bae0f71a7..2613f6ae0 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm
@@ -5,12 +5,6 @@ use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
-sub begin : Private {
- my ( $self, $c ) = @_;
-
- $c->forward('/admin/begin');
-}
-
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
@@ -71,6 +65,7 @@ sub edit : Path : Args(2) {
$priority->name( $c->get_param('name') );
$priority->description( $c->get_param('description') );
$priority->external_id( $c->get_param('external_id') );
+ $priority->is_default( $c->get_param('is_default') ? 1 : 0 );
$priority->update_or_insert;
my @live_contact_ids = map { $_->id } @live_contacts;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/States.pm b/perllib/FixMyStreet/App/Controller/Admin/States.pm
new file mode 100644
index 000000000..938692af0
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/States.pm
@@ -0,0 +1,96 @@
+package FixMyStreet::App::Controller::Admin::States;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->forward('/auth/get_csrf_token');
+ $c->forward('/admin/fetch_languages');
+ my $rs = $c->model('DB::State');
+
+ if ($c->req->method eq 'POST') {
+ $c->forward('/auth/check_csrf_token');
+
+ $c->forward('process_new')
+ && $c->forward('delete')
+ && $c->forward('update');
+
+ $rs->clear;
+ }
+
+ $c->stash->{open_states} = $rs->open;
+ $c->stash->{closed_states} = $rs->closed;
+ $c->stash->{fixed_states} = $rs->fixed;
+}
+
+sub process_new : Private {
+ my ($self, $c) = @_;
+ if ($c->get_param('new_fixed')) {
+ $c->model('DB::State')->create({
+ label => 'fixed',
+ type => 'fixed',
+ name => _('Fixed'),
+ });
+ return 0;
+ }
+ return 1 unless $c->get_param('new');
+ my %params = map { $_ => $c->get_param($_) } qw/label type name/;
+ $c->model('DB::State')->create(\%params);
+ return 0;
+}
+
+sub delete : Private {
+ my ($self, $c) = @_;
+
+ my @params = keys %{ $c->req->params };
+ my ($to_delete) = map { /^delete:(.*)/ } grep { /^delete:/ } @params;
+ if ($to_delete) {
+ $c->model('DB::State')->search({ label => $to_delete })->delete;
+ return 0;
+ }
+ return 1;
+}
+
+sub update : Private {
+ my ($self, $c) = @_;
+
+ my $rs = $c->model('DB::State');
+ my %db_states = map { $_->label => $_ } @{$rs->states};
+ my @params = keys %{ $c->req->params };
+ my @states = map { /^type:(.*)/ } grep { /^type:/ } @params;
+
+ foreach my $state (@states) {
+ # If there is only one language, we still store confirmed/closed
+ # as translations, as that seems a sensible place to store them.
+ if ($state eq 'confirmed' or $state eq 'closed') {
+ if (my $name = $c->get_param("name:$state")) {
+ my ($lang) = keys %{$c->stash->{languages}};
+ $db_states{$state}->add_translation_for('name', $lang, $name);
+ }
+ } else {
+ $db_states{$state}->update({
+ type => $c->get_param("type:$state"),
+ name => $c->get_param("name:$state"),
+ });
+ }
+
+ foreach my $lang (keys(%{$c->stash->{languages}})) {
+ my $id = $c->get_param("translation_id:$state:$lang");
+ my $text = $c->get_param("translation:$state:$lang");
+ if ($text) {
+ $db_states{$state}->add_translation_for('name', $lang, $text);
+ } elsif ($id) {
+ $c->model('DB::Translation')->find({ id => $id })->delete;
+ }
+ }
+ }
+
+ return 1;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm
index 1fe35d0a3..b872084ff 100644
--- a/perllib/FixMyStreet/App/Controller/Around.pm
+++ b/perllib/FixMyStreet/App/Controller/Around.pm
@@ -163,52 +163,20 @@ sub display_location : Private {
$c->forward('/auth/get_csrf_token');
- # get the lat,lng
- my $latitude = $c->stash->{latitude};
- my $longitude = $c->stash->{longitude};
-
- # Deal with pin hiding/age
- my $all_pins = $c->get_param('all_pins') ? 1 : undef;
- $c->stash->{all_pins} = $all_pins;
- my $interval = $all_pins ? undef : $c->cobrand->on_map_default_max_pin_age;
-
- $c->forward( '/reports/stash_report_filter_status' );
-
# Check the category to filter by, if any, is valid
$c->forward('check_and_stash_category');
- $c->forward( '/reports/stash_report_sort', [ 'created-desc' ]);
-
- # get the map features
- my ( $on_map_all, $on_map, $nearby, $distance ) =
- FixMyStreet::Map::map_features( $c,
- latitude => $latitude, longitude => $longitude,
- interval => $interval, categories => [ keys %{$c->stash->{filter_category}} ],
- states => $c->stash->{filter_problem_states},
- order => $c->stash->{sort_order},
- );
- # copy the found reports to the stash
- $c->stash->{on_map} = $on_map;
- $c->stash->{around_map} = $nearby;
- $c->stash->{distance} = $distance;
+ my $latitude = $c->stash->{latitude};
+ my $longitude = $c->stash->{longitude};
- # create a list of all the pins
- my @pins;
- unless ($c->get_param('no_pins')) {
- @pins = map {
- # Here we might have a DB::Problem or a DB::Nearby, we always want the problem.
- my $p = (ref $_ eq 'FixMyStreet::App::Model::DB::Nearby') ? $_->problem : $_;
- $p->pin_data($c, 'around');
- } @$on_map_all, @$nearby;
- }
+ $c->forward('map_features', [ { latitude => $latitude, longitude => $longitude } ] );
- $c->stash->{page} = 'around'; # So the map knows to make clickable pins, update on pan
FixMyStreet::Map::display_map(
$c,
latitude => $latitude,
longitude => $longitude,
clickable => 1,
- pins => \@pins,
+ pins => $c->stash->{pins},
area => $c->cobrand->areas_on_around,
);
@@ -259,7 +227,7 @@ sub check_and_stash_category : Private {
distinct => 1
}
)->all;
- my @categories = map { $_->category } @contacts;
+ my @categories = map { { name => $_->category, value => $_->category_display } } @contacts;
$c->stash->{filter_categories} = \@categories;
my %categories_mapped = map { $_ => 1 } @categories;
@@ -268,6 +236,44 @@ sub check_and_stash_category : Private {
$c->stash->{filter_category} = \%valid_categories;
}
+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->forward( '/reports/stash_report_filter_status' );
+ $c->forward( '/reports/stash_report_sort', [ 'created-desc' ]);
+
+ # Deal with pin hiding/age
+ my $all_pins = $c->get_param('all_pins') ? 1 : undef;
+ $c->stash->{all_pins} = $all_pins;
+ my $interval = $all_pins ? undef : $c->cobrand->on_map_default_max_pin_age;
+
+ return if $c->get_param('js'); # JS will request the same (or more) data client side
+
+ my ( $on_map_all, $on_map_list, $nearby, $distance ) =
+ FixMyStreet::Map::map_features(
+ $c, interval => $interval, %$extra,
+ categories => [ keys %{$c->stash->{filter_category}} ],
+ states => $c->stash->{filter_problem_states},
+ order => $c->stash->{sort_order},
+ );
+
+ my @pins;
+ unless ($c->get_param('no_pins')) {
+ @pins = map {
+ # Here we might have a DB::Problem or a DB::Result::Nearby, we always want the problem.
+ my $p = (ref $_ eq 'FixMyStreet::DB::Result::Nearby') ? $_->problem : $_;
+ $p->pin_data($c, 'around');
+ } @$on_map_all, @$nearby;
+ }
+
+ $c->stash->{pins} = \@pins;
+ $c->stash->{on_map} = $on_map_list;
+ $c->stash->{around_map} = $nearby;
+ $c->stash->{distance} = $distance;
+}
+
=head2 /ajax
Handle the ajax calls that the map makes when it is dragged. The info returned
@@ -279,8 +285,6 @@ the map.
sub ajax : Path('/ajax') {
my ( $self, $c ) = @_;
- $c->res->content_type('application/json; charset=utf-8');
-
my $bbox = $c->get_param('bbox');
unless ($bbox) {
$c->res->status(404);
@@ -288,52 +292,13 @@ sub ajax : Path('/ajax') {
return;
}
- # assume this is not cacheable - may need to be more fine-grained later
- $c->res->header( 'Cache_Control' => 'max-age=0' );
-
- $c->stash->{page} = 'around'; # Needed by _item.html
-
- # how far back should we go?
- my $all_pins = $c->get_param('all_pins') ? 1 : undef;
- my $interval = $all_pins ? undef : $c->cobrand->on_map_default_max_pin_age;
-
- $c->forward( '/reports/stash_report_filter_status' );
- $c->forward( '/reports/stash_report_sort', [ 'created-desc' ]);
-
- # extract the data from the map
- my ( $on_map_all, $on_map_list, $nearby, $dist ) =
- FixMyStreet::Map::map_features($c,
- bbox => $bbox, interval => $interval,
- categories => [ $c->get_param_list('filter_category', 1) ],
- states => $c->stash->{filter_problem_states},
- order => $c->stash->{sort_order},
- );
-
- # create a list of all the pins
- my @pins = map {
- # Here we might have a DB::Problem or a DB::Nearby, we always want the problem.
- my $p = (ref $_ eq 'FixMyStreet::App::Model::DB::Nearby') ? $_->problem : $_;
- my $colour = $c->cobrand->pin_colour( $p, 'around' );
- [ $p->latitude, $p->longitude,
- $colour,
- $p->id, $p->title_safe
- ]
- } @$on_map_all, @$nearby;
-
- # render templates to get the html
- my $on_map_list_html = $c->render_fragment(
- 'around/on_map_list_items.html',
- { on_map => $on_map_list, around_map => $nearby }
- );
+ my %valid_categories = map { $_ => 1 } $c->get_param_list('filter_category', 1);
+ $c->stash->{filter_category} = \%valid_categories;
- # JSON encode the response
- my $json = { pins => \@pins };
- $json->{current} = $on_map_list_html if $on_map_list_html;
- my $body = encode_json($json);
- $c->res->body($body);
+ $c->forward('map_features', [ { bbox => $bbox } ]);
+ $c->forward('/reports/ajax', [ 'around/on_map_list_items.html' ]);
}
-
sub location_autocomplete : Path('/ajax/geocode') {
my ( $self, $c ) = @_;
$c->res->content_type('application/json; charset=utf-8');
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index 4efa7abb8..825066026 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -33,7 +33,7 @@ sub general : Path : Args(0) {
my ( $self, $c ) = @_;
$c->detach( 'redirect_on_signin', [ $c->get_param('r') ] )
- if $c->user && $c->get_param('r');
+ if $c->req->method eq 'GET' && $c->user && $c->get_param('r');
# all done unless we have a form posted to us
return unless $c->req->method eq 'POST';
@@ -128,6 +128,18 @@ sub email_sign_in : Private {
return;
}
+ # If user registration is disabled then bail out at this point
+ # if there's not already a user with this email address.
+ # NB this uses the same template as a successful sign in to stop
+ # enumeration of valid email addresses.
+ if ( FixMyStreet->config('SIGNUPS_DISABLED')
+ && !$c->model('DB::User')->search({ email => $good_email })->count
+ && !$c->stash->{current_user} # don't break the change email flow
+ ) {
+ $c->stash->{template} = 'auth/token.html';
+ return;
+ }
+
my $user_params = {};
$user_params->{password} = $c->get_param('password_register')
if $c->get_param('password_register');
@@ -199,6 +211,10 @@ sub token : Path('/M') : Args(1) {
my $user = $c->model('DB::User')->find_or_new({ email => $data->{email} });
+ # Bail out if this is a new user and SIGNUPS_DISABLED is set
+ $c->detach( '/page_error_403_access_denied', [] )
+ if FixMyStreet->config('SIGNUPS_DISABLED') && !$user->in_storage && !$data->{old_email};
+
if ($data->{old_email}) {
# Were logged in as old_email, want to switch to email ($user)
if ($user->in_storage) {
@@ -244,6 +260,8 @@ sub fb : Private {
sub facebook_sign_in : Private {
my ( $self, $c ) = @_;
+ $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
+
my $fb = $c->forward('/auth/fb');
my $url = $fb->get_authorization_url(scope => ['email']);
@@ -302,6 +320,8 @@ sub tw : Private {
sub twitter_sign_in : Private {
my ( $self, $c ) = @_;
+ $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
+
my $twitter = $c->forward('/auth/tw');
my $url = $twitter->get_authentication_url(callback => $c->uri_for('/auth/Twitter'));
diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm
index b98bdbcc7..f2c3be47c 100644
--- a/perllib/FixMyStreet/App/Controller/Contact.pm
+++ b/perllib/FixMyStreet/App/Controller/Contact.pm
@@ -54,7 +54,8 @@ sub submit : Path('submit') : Args(0) {
&& $c->forward('determine_contact_type')
&& $c->forward('validate')
&& $c->forward('prepare_params_for_email')
- && $c->forward('send_email');
+ && $c->forward('send_email')
+ && $c->forward('redirect_on_success');
}
=head2 determine_contact_type
@@ -99,7 +100,7 @@ sub determine_contact_type : Private {
=head2 validate
-Validate the form submission parameters. Sets error messages and redirect
+Validate the form submission parameters. Sets error messages and redirect
to index page if errors.
=cut
@@ -168,8 +169,7 @@ sub prepare_params_for_email : Private {
if ( $c->stash->{update} ) {
- $c->stash->{problem_url} = $base_url . '/report/' . $c->stash->{update}->problem_id
- . '#update_' . $c->stash->{update}->id;
+ $c->stash->{problem_url} = $base_url . $c->stash->{update}->url;
$c->stash->{admin_url} = $admin_url . '/update_edit/' . $c->stash->{update}->id;
$c->stash->{complaint} = sprintf(
"Complaint about update %d on report %d",
@@ -258,10 +258,24 @@ sub send_email : Private {
$params->{from} = $from;
}
- $c->send_email('contact.txt', $params);
+ $c->stash->{success} = $c->send_email('contact.txt', $params);
- # above is always succesful :(
- $c->stash->{success} = 1;
+ return 1;
+}
+
+=head2 redirect_on_success
+
+Redirect to a custom URL if one was provided
+
+=cut
+
+sub redirect_on_success : Private {
+ my ( $self, $c ) = @_;
+
+ if (my $success_url = $c->get_param('success_url')) {
+ $c->res->redirect($success_url);
+ $c->detach;
+ }
return 1;
}
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index fbe5a2dc9..f961660c0 100644
--- a/perllib/FixMyStreet/App/Controller/Dashboard.pm
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -24,6 +24,8 @@ sub example : Local : Args(0) {
my ( $self, $c ) = @_;
$c->stash->{template} = 'dashboard/index.html';
+ $c->stash->{filter_states} = $c->cobrand->state_groups_inspect;
+
$c->stash->{children} = {};
for my $i (1..3) {
$c->stash->{children}{$i} = { id => $i, name => "Ward $i" };
@@ -93,6 +95,7 @@ sub index : Path : Args(0) {
$c->stash->{body} = $body;
# Set up the data for the dropdowns
+ $c->stash->{filter_states} = $c->cobrand->state_groups_inspect;
# Just take the first area ID we find
my $area_id = $body->body_areas->first->area_id;
@@ -145,12 +148,10 @@ sub index : Path : Args(0) {
# List of reports underneath summary table
$c->stash->{q_state} = $c->get_param('state') || '';
- if ( $c->stash->{q_state} eq 'fixed' ) {
+ if ( $c->stash->{q_state} eq 'fixed - council' ) {
$prob_where->{'me.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
} elsif ( $c->stash->{q_state} ) {
$prob_where->{'me.state'} = $c->stash->{q_state};
- $prob_where->{'me.state'} = { IN => [ 'planned', 'action scheduled' ] }
- if $prob_where->{'me.state'} eq 'action scheduled';
}
my $params = {
%$prob_where,
@@ -180,7 +181,7 @@ sub export_as_csv {
my ($self, $c, $problems_rs, $body) = @_;
require Text::CSV;
my $problems = $problems_rs->search(
- {}, { prefetch => 'comments' });
+ {}, { prefetch => 'comments', order_by => 'me.confirmed' });
my $filename = do {
my %where = (
@@ -211,6 +212,9 @@ sub export_as_csv {
'Status',
'Latitude', 'Longitude',
'Nearest Postcode',
+ 'Ward',
+ 'Easting',
+ 'Northing',
'Report URL',
);
my @body = ($csv->string);
@@ -242,6 +246,13 @@ sub export_as_csv {
}
}
+ my $wards = join ', ',
+ map { $c->stash->{children}->{$_}->{name} }
+ grep {$c->stash->{children}->{$_} }
+ split ',', $hashref->{areas};
+
+ my @local_coords = $report->local_coords;
+
$csv->combine(
@{$hashref}{
'id',
@@ -258,6 +269,9 @@ sub export_as_csv {
'latitude', 'longitude',
'postcode',
},
+ $wards,
+ $local_coords[0],
+ $local_coords[1],
(join '', $c->cobrand->base_url_for_report($report), $report->url),
);
diff --git a/perllib/FixMyStreet/App/Controller/JSON.pm b/perllib/FixMyStreet/App/Controller/JSON.pm
index d3cd33546..762e3c115 100644
--- a/perllib/FixMyStreet/App/Controller/JSON.pm
+++ b/perllib/FixMyStreet/App/Controller/JSON.pm
@@ -105,10 +105,9 @@ sub problems : Local {
foreach my $problem (@problems) {
$problem->name( '' ) if $problem->anonymous == 1;
$problem->service( 'Web interface' ) if $problem->service eq '';
- my $bodies = $problem->bodies;
- if (keys %$bodies) {
- my @body_names = map { $_->name } values %$bodies;
- $problem->bodies_str( join(' and ', @body_names) );
+ my $body_names = $problem->body_names;
+ if (@$body_names) {
+ $problem->bodies_str( join(' and ', @$body_names) );
}
}
diff --git a/perllib/FixMyStreet/App/Controller/Location.pm b/perllib/FixMyStreet/App/Controller/Location.pm
index 6a0f2c0ec..c457c8fce 100644
--- a/perllib/FixMyStreet/App/Controller/Location.pm
+++ b/perllib/FixMyStreet/App/Controller/Location.pm
@@ -31,8 +31,6 @@ sub determine_location_from_coords : Private {
my $latitude = $c->get_param('latitude') || $c->get_param('lat');
my $longitude = $c->get_param('longitude') || $c->get_param('lon');
- $c->log->debug($longitude);
- $c->log->debug($latitude);
if ( defined $latitude && defined $longitude ) {
($c->stash->{latitude}, $c->stash->{longitude}) =
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index 74f2e6b31..e2ab16b6b 100644
--- a/perllib/FixMyStreet/App/Controller/Moderate.pm
+++ b/perllib/FixMyStreet/App/Controller/Moderate.pm
@@ -146,7 +146,7 @@ sub report_moderate_title : Private {
my $title = $c->get_param('problem_revert_title') ?
$original_title
- : $self->diff($original_title, $c->get_param('problem_title'));
+ : $c->get_param('problem_title');
if ($title ne $old_title) {
$original->insert unless $original->in_storage;
@@ -167,7 +167,7 @@ sub report_moderate_detail : Private {
my $original_detail = $original->detail;
my $detail = $c->get_param('problem_revert_detail') ?
$original_detail
- : $self->diff($original_detail, $c->get_param('problem_detail'));
+ : $c->get_param('problem_detail');
if ($detail ne $old_detail) {
$original->insert unless $original->in_storage;
@@ -285,7 +285,7 @@ sub update_moderate_detail : Private {
my $original_detail = $original->detail;
my $detail = $c->get_param('update_revert_detail') ?
$original_detail
- : $self->diff($original_detail, $c->get_param('update_detail'));
+ : $c->get_param('update_detail');
if ($detail ne $old_detail) {
$original->insert unless $original->in_storage;
@@ -340,29 +340,6 @@ sub return_text : Private {
$c->res->body( $text // '' );
}
-sub diff {
- my ($self, $old, $new) = @_;
-
- $new =~s/\[\.{3}\]//g;
-
- my $diff = Algorithm::Diff->new( [ split //, $old ], [ split //, $new ] );
- my $string;
- while ($diff->Next) {
- my $d = $diff->Diff;
- if ($d & 1) {
- my $deleted = join '', $diff->Items(1);
- unless ($deleted =~/^\s*$/) {
- $string .= ' ' if $deleted =~/^ /;
- $string .= '[...]';
- $string .= ' ' if $deleted =~/ $/;
- }
- }
- $string .= join '', $diff->Items(2);
- }
- return $string;
-}
-
-
__PACKAGE__->meta->make_immutable;
1;
diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm
index 77711f807..5b80a4a08 100644
--- a/perllib/FixMyStreet/App/Controller/My.pm
+++ b/perllib/FixMyStreet/App/Controller/My.pm
@@ -19,9 +19,10 @@ Catalyst Controller.
=cut
-sub begin : Private {
+sub auto : Private {
my ($self, $c) = @_;
$c->detach( '/auth/redirect' ) unless $c->user;
+ return 1;
}
=head2 index
@@ -162,7 +163,7 @@ sub setup_page_data : Private {
distinct => 1,
order_by => [ 'category' ],
} )->all;
- @categories = map { $_->category } @categories;
+ @categories = map { { name => $_->category, value => $_->category_display } } @categories;
$c->stash->{filter_categories} = \@categories;
$c->stash->{page} = 'my';
@@ -205,6 +206,20 @@ sub planned_change : Path('planned/change') {
}
}
+sub shortlist_multiple : Path('planned/change_multiple') {
+ my ($self, $c) = @_;
+ $c->forward('/auth/check_csrf_token');
+
+ my @ids = $c->get_param_list('ids[]');
+
+ foreach my $id (@ids) {
+ $c->forward( '/report/load_problem_or_display_error', [ $id ] );
+ $c->user->add_to_planned_reports($c->stash->{problem});
+ }
+
+ $c->res->body(encode_json({ outcome => 'add' }));
+}
+
sub by_shortlisted {
my $a_order = $a->get_extra_metadata('order') || 0;
my $b_order = $b->get_extra_metadata('order') || 0;
@@ -220,6 +235,38 @@ sub by_shortlisted {
}
}
+sub anonymize : Path('anonymize') {
+ my ($self, $c) = @_;
+ $c->forward('/auth/get_csrf_token');
+
+ my $object;
+ if (my $id = $c->get_param('problem')) {
+ $c->forward( '/report/load_problem_or_display_error', [ $id ] );
+ $object = $c->stash->{problem};
+ } elsif ($id = $c->get_param('update')) {
+ $c->stash->{update} = $object = $c->model('DB::Comment')->find({ id => $id });
+ $c->detach('/page_error_400_bad_request') unless $object;
+ } else {
+ $c->detach('/page_error_404_not_found');
+ }
+ $c->detach('/page_error_400_bad_request') unless $c->user->id == $object->user_id;
+ $c->detach('/page_error_400_bad_request') if $object->anonymous;
+
+ if ($c->get_param('hide') || $c->get_param('hide_everywhere')) {
+ $c->detach('/page_error_400_bad_request') unless $c->req->method eq 'POST';
+ $c->forward('/auth/check_csrf_token');
+ if ($c->get_param('hide')) {
+ $object->update({ anonymous => 1 });
+ $c->flash->{anonymized} = _('Your name has been hidden.');
+ } elsif ($c->get_param('hide_everywhere')) {
+ $c->user->problems->update({anonymous => 1});
+ $c->user->comments->update({anonymous => 1});
+ $c->flash->{anonymized} = _('Your name has been hidden from all your reports and updates.');
+ }
+ $c->res->redirect($object->url);
+ }
+}
+
__PACKAGE__->meta->make_immutable;
1;
diff --git a/perllib/FixMyStreet/App/Controller/Open311.pm b/perllib/FixMyStreet/App/Controller/Open311.pm
index bc08593de..95b29d116 100644
--- a/perllib/FixMyStreet/App/Controller/Open311.pm
+++ b/perllib/FixMyStreet/App/Controller/Open311.pm
@@ -160,7 +160,7 @@ sub get_services : Private {
my $lon = $c->get_param('long') || '';
# Look up categories for this council or councils
- my $categories = $c->model('DB::Contact')->not_deleted;
+ my $categories = $c->model('DB::Contact')->active;
if ($lat || $lon) {
my $area_types = $c->cobrand->area_types;
@@ -241,7 +241,8 @@ sub output_requests : Private {
'long' => $problem->longitude,
'status' => $problem->state,
# 'status_notes' => {},
- 'requested_datetime' => w3date($problem->confirmed),
+ # Zurich has visible unconfirmed reports
+ 'requested_datetime' => w3date($problem->confirmed || $problem->created),
'updated_datetime' => w3date($problem->lastupdate),
# 'expected_datetime' => {},
# 'address' => {},
@@ -258,8 +259,8 @@ sub output_requests : Private {
}
else {
# FIXME Not according to Open311 v2
- my @body_names = map { $_->name } values %{$problem->bodies};
- $request->{agency_responsible} = {'recipient' => [ @body_names ] };
+ my $body_names = $problem->body_names;
+ $request->{agency_responsible} = {'recipient' => $body_names };
}
if ( !$problem->anonymous ) {
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm
index ad2702460..e37e08698 100644
--- a/perllib/FixMyStreet/App/Controller/Report.pm
+++ b/perllib/FixMyStreet/App/Controller/Report.pm
@@ -28,14 +28,11 @@ Redirect to homepage unless C<id> parameter in query, in which case redirect to
sub index : Path('') : Args(0) {
my ( $self, $c ) = @_;
- my $id = $c->get_param('id');
-
- my $uri =
- $id
- ? $c->uri_for( '/report', $id )
- : $c->uri_for('/');
-
- $c->res->redirect($uri);
+ if ($c->stash->{homepage_template}) {
+ $c->stash->{template} = 'index.html';
+ } else {
+ $c->res->redirect('/');
+ }
}
=head2 report_display
@@ -124,7 +121,7 @@ sub load_problem_or_display_error : Private {
$c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] )
unless $c->cobrand->show_unconfirmed_reports ;
}
- elsif ( $problem->hidden_states->{ $problem->state } or
+ elsif ( $problem->hidden_states->{ $problem->state } or
(($problem->get_extra_metadata('closure_status')||'') eq 'hidden')) {
$c->detach(
'/page_error_410_gone',
@@ -159,7 +156,7 @@ sub load_updates : Private {
my $updates = $c->model('DB::Comment')->search(
{ problem_id => $c->stash->{problem}->id, state => 'confirmed' },
- { order_by => 'confirmed' }
+ { order_by => [ 'confirmed', 'id' ] }
);
my $questionnaires = $c->model('DB::Questionnaire')->search(
@@ -181,8 +178,10 @@ sub load_updates : Private {
@combined = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @combined;
$c->stash->{updates} = \@combined;
- if ($c->sessionid && $c->flash->{alert_to_reporter}) {
- $c->stash->{alert_to_reporter} = 1;
+ if ($c->sessionid) {
+ foreach (qw(alert_to_reporter anonymized)) {
+ $c->stash->{$_} = $c->flash->{$_} if $c->flash->{$_};
+ }
}
return 1;
@@ -201,7 +200,8 @@ sub format_problem_for_display : Private {
$c->stash->{add_alert} = 1;
}
- $c->stash->{extra_name_info} = $problem->bodies_str && $problem->bodies_str eq '2482' ? 1 : 0;
+ my $first_body = (values %{$problem->bodies})[0];
+ $c->stash->{extra_name_info} = $first_body && $first_body->name =~ /Bromley/ ? 1 : 0;
$c->forward('generate_map_tags');
@@ -309,38 +309,33 @@ sub inspect : Private {
$c->forward('/admin/categories_for_point');
$c->stash->{report_meta} = { map { $_->{name} => $_ } @{ $c->stash->{problem}->get_extra_fields() } };
- my %category_body = map { $_->category => $_->body_id } map { $_->contacts->all } values %{$problem->bodies};
-
- my @priorities = $c->model('DB::ResponsePriority')->for_bodies($problem->bodies_str_ids)->all;
- my $priorities_by_category = {};
- foreach my $pri (@priorities) {
- my $any = 0;
- foreach ($pri->contacts->all) {
- $any = 1;
- push @{$priorities_by_category->{$_->category}}, $pri->id . '=' . URI::Escape::uri_escape_utf8($pri->name);
- }
- if (!$any) {
- foreach (grep { $category_body{$_} == $pri->body_id } @{$c->stash->{categories}}) {
- push @{$priorities_by_category->{$_}}, $pri->id . '=' . URI::Escape::uri_escape_utf8($pri->name);
- }
- }
+ 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}});
+ $c->stash->{priorities_by_category} = $priorities_by_category;
+ my $templates_by_category = FixMyStreet::App->model('DB::ResponseTemplate')->by_categories($c->cobrand->council_area_id, @{$c->stash->{contacts}});
+ $c->stash->{templates_by_category} = $templates_by_category;
}
- foreach (keys %{$priorities_by_category}) {
- $priorities_by_category->{$_} = join('&', @{$priorities_by_category->{$_}});
+
+ if ($c->user->has_body_permission_to('planned_reports')) {
+ $c->stash->{post_inspect_url} = $c->req->referer;
}
- $c->stash->{priorities_by_category} = $priorities_by_category;
+ if ($c->user->has_body_permission_to('report_edit_priority') or
+ $c->user->has_body_permission_to('report_inspect')
+ ) {
+ $c->stash->{has_default_priority} = scalar( grep { $_->is_default } $problem->response_priorities );
+ }
if ( $c->get_param('save') ) {
$c->forward('/auth/check_csrf_token');
my $valid = 1;
- my $update_text;
+ my $update_text = '';
my $reputation_change = 0;
my %update_params = ();
if ($permissions->{report_inspect}) {
- foreach (qw/detailed_information traffic_information duplicate_of/) {
+ foreach (qw/detailed_information traffic_information/) {
$problem->set_extra_metadata( $_ => $c->get_param($_) );
}
@@ -375,19 +370,35 @@ sub inspect : Private {
}
if ( $problem->state ne 'duplicate' ) {
$problem->unset_extra_metadata('duplicate_of');
+ } elsif (my $duplicate_of = $c->get_param('duplicate_of')) {
+ $problem->set_duplicate_of($duplicate_of);
}
+
if ( $problem->state ne $old_state ) {
$c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'state_change' ] );
- # If the state has been changed by an inspector, consider the
- # report to be inspected.
- unless ($problem->get_extra_metadata('inspected')) {
- $problem->set_extra_metadata( inspected => 1 );
- $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'inspected' ] );
- my $state = $problem->state;
- $reputation_change = 1 if $c->cobrand->reputation_increment_states->{$state};
- $reputation_change = -1 if $c->cobrand->reputation_decrement_states->{$state};
- }
+ $update_params{problem_state} = $problem->state;
+
+ my $state = $problem->state;
+ $reputation_change = 1 if $c->cobrand->reputation_increment_states->{$state};
+ $reputation_change = -1 if $c->cobrand->reputation_decrement_states->{$state};
+
+ # If an inspector has changed the state, subscribe them to
+ # updates
+ my $options = {
+ cobrand => $c->cobrand->moniker,
+ cobrand_data => $problem->cobrand_data,
+ lang => $problem->lang,
+ };
+ $problem->user->create_alert($problem->id, $options);
+ }
+
+ # If the state has been changed to action scheduled and they've said
+ # they want to raise a defect, consider the report to be inspected.
+ if ($problem->state eq 'action scheduled' && $c->get_param('raise_defect') && !$problem->get_extra_metadata('inspected')) {
+ $update_params{extra} = { 'defect_raised' => 1 };
+ $problem->set_extra_metadata( inspected => 1 );
+ $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'inspected' ] );
}
}
@@ -421,39 +432,52 @@ sub inspect : Private {
}
$problem->lastupdate( \'current_timestamp' );
$problem->update;
- if ( defined($update_text) ) {
- my $timestamp = \'current_timestamp';
- if (my $saved_at = $c->get_param('saved_at')) {
- $timestamp = DateTime->from_epoch( epoch => $saved_at );
- }
- my $name = $c->user->from_body ? $c->user->from_body->name : $c->user->name;
- $problem->add_to_comments( {
- text => $update_text,
- created => $timestamp,
- confirmed => $timestamp,
- user_id => $c->user->id,
- name => $name,
- state => 'confirmed',
- mark_fixed => 0,
- anonymous => 0,
- %update_params,
- } );
+ my $timestamp = \'current_timestamp';
+ if (my $saved_at = $c->get_param('saved_at')) {
+ $timestamp = DateTime->from_epoch( epoch => $saved_at );
}
- # This problem might no longer be visible on the current cobrand,
- # if its body has changed (e.g. by virtue of the category changing)
- # so redirect to a cobrand where it can be seen if necessary
+ my $name = $c->user->from_body ? $c->user->from_body->name : $c->user->name;
+ $problem->add_to_comments( {
+ text => $update_text,
+ created => $timestamp,
+ confirmed => $timestamp,
+ user_id => $c->user->id,
+ name => $name,
+ state => 'confirmed',
+ mark_fixed => 0,
+ anonymous => 0,
+ %update_params,
+ } );
+
my $redirect_uri;
- if ( $c->cobrand->is_council && !$c->cobrand->owns_problem($problem) ) {
+ $problem->discard_changes;
+
+ # If inspector, redirect back to the map view they came from
+ # with the right filters. If that wasn't set, go to /around at this
+ # report's location.
+ # We go here rather than the shortlist because it makes it much
+ # simpler to inspect many reports in the same location. The
+ # 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 $params = {
+ lat => $problem->latitude,
+ lon => $problem->longitude,
+ };
+ $params->{filter_category} = $categories if $categories;
+ $params->{js} = 1 if $c->get_param('js');
+ $redirect_uri = $c->uri_for( "/around", $params );
+ }
+ } elsif ( $c->cobrand->is_council && !$c->cobrand->owns_problem($problem) ) {
+ # This problem might no longer be visible on the current cobrand,
+ # if its body has changed (e.g. by virtue of the category changing)
+ # so redirect to a cobrand where it can be seen if necessary
$redirect_uri = $c->cobrand->base_url_for_report( $problem ) . $problem->url;
} else {
$redirect_uri = $c->uri_for( $problem->url );
}
- # Or if inspector, redirect back to shortlist
- if ($c->user->has_body_permission_to('planned_reports')) {
- $redirect_uri = $c->uri_for_action('my/planned');
- }
-
$c->log->debug( "Redirecting to: " . $redirect_uri );
$c->res->redirect( $redirect_uri );
}
@@ -476,7 +500,7 @@ sub nearby_json : Private {
$c->forward( 'load_problem_or_display_error', [ $id ] );
my $p = $c->stash->{problem};
- my $dist = 1000;
+ my $dist = 1;
my $nearby = $c->model('DB::Nearby')->nearby(
$c, $dist, [ $p->id ], 5, $p->latitude, $p->longitude, undef, [ $p->category ], undef
@@ -496,7 +520,7 @@ sub nearby_json : Private {
);
my $json = { pins => \@pins };
- $json->{current} = $on_map_list_html if $on_map_list_html;
+ $json->{reports_list} = $on_map_list_html if $on_map_list_html;
my $body = encode_json($json);
$c->res->content_type('application/json; charset=utf-8');
$c->res->body($body);
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index 2a68b170e..f92a5cb22 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -99,12 +99,14 @@ sub report_new : Path : Args(0) {
# create a problem from the submitted details
$c->stash->{template} = "report/new/fill_in_details.html";
$c->forward('setup_categories_and_bodies');
+ $c->forward('setup_report_extra_fields');
$c->forward('generate_map');
$c->forward('check_for_category');
# deal with the user and report and check both are happy
return unless $c->forward('check_form_submitted');
+
$c->forward('/auth/check_csrf_token');
$c->forward('process_user');
$c->forward('process_report');
@@ -137,6 +139,7 @@ sub report_new_ajax : Path('mobile') : Args(0) {
}
$c->forward('setup_categories_and_bodies');
+ $c->forward('setup_report_extra_fields');
$c->forward('process_user');
$c->forward('process_report');
$c->forward('/photo/process_photo');
@@ -184,6 +187,7 @@ sub report_form_ajax : Path('ajax') : Args(0) {
}
$c->forward('setup_categories_and_bodies');
+ $c->forward('setup_report_extra_fields');
# render templates to get the html
my $category = $c->render_fragment( 'report/new/category.html');
@@ -200,8 +204,10 @@ sub report_form_ajax : Path('ajax') : Args(0) {
if ($c->user_exists) {
my @bodies = keys %{$c->stash->{bodies}};
my $ca_another_user = $c->user->has_permission_to('contribute_as_another_user', \@bodies);
- my $ca_body = $c->user->has_permission_to('contribute_as_body', \@bodies);
+ my $ca_anonymous_user = $c->user->has_permission_to('contribute_as_anonymous_user', \@bodies);
+ my $ca_body = $c->user->from_body && $c->user->has_permission_to('contribute_as_body', \@bodies);
$contribute_as->{another_user} = $ca_another_user if $ca_another_user;
+ $contribute_as->{anonymous_user} = $ca_anonymous_user if $ca_anonymous_user;
$contribute_as->{body} = $ca_body if $ca_body;
}
@@ -212,7 +218,6 @@ sub report_form_ajax : Path('ajax') : Args(0) {
category => $category,
extra_name_info => $extra_name_info,
titles_list => $extra_titles_list,
- categories => $c->stash->{category_options},
%$contribute_as ? (contribute_as => $contribute_as) : (),
$top_message ? (top_message => $top_message) : (),
}
@@ -233,6 +238,7 @@ sub category_extras_ajax : Path('category_extras') : Args(0) {
return 1;
}
$c->forward('setup_categories_and_bodies');
+ $c->forward('setup_report_extra_fields');
$c->forward('check_for_category');
my $category = $c->stash->{category} || "";
@@ -252,6 +258,9 @@ sub category_extras_ajax : Path('category_extras') : Args(0) {
if ($c->stash->{unresponsive}->{$category}) {
$generate = 1;
}
+ if ($c->stash->{report_extra_fields}) {
+ $generate = 1;
+ }
if ($generate) {
$category_extra = $c->render_fragment('report/new/category_extras.html', $vars);
}
@@ -493,7 +502,7 @@ Work out what the location of the report should be - either by using lat,lng or
a tile click or what's come in from a partial. Returns false if no location
could be found.
-=cut
+=cut
sub determine_location : Private {
my ( $self, $c ) = @_;
@@ -515,7 +524,7 @@ sub determine_location : Private {
Detect that the map tiles have been clicked on by looking for the tile
parameters.
-=cut
+=cut
sub determine_location_from_tile_click : Private {
my ( $self, $c ) = @_;
@@ -566,7 +575,7 @@ sub determine_location_from_tile_click : Private {
Use latitude and longitude stored in the report - this is probably result of a
partial report being loaded.
-=cut
+=cut
sub determine_location_from_report : Private {
my ( $self, $c ) = @_;
@@ -604,8 +613,8 @@ sub setup_categories_and_bodies : Private {
my $contacts #
= $c #
->model('DB::Contact') #
- ->not_deleted #
- ->search( { body_id => [ keys %bodies ] } );
+ ->active
+ ->search( { body_id => [ keys %bodies ] }, { prefetch => 'body' } );
my @contacts = $c->cobrand->categories_restriction($contacts)->all;
# variables to populate
@@ -632,13 +641,19 @@ sub setup_categories_and_bodies : Private {
# keysort does not appear to obey locale so use strcoll (see i18n.t)
@contacts = sort { strcoll( $a->category, $b->category ) } @contacts;
+ # Get defect types for inspectors
+ if ($c->cobrand->can('council_area_id')) {
+ my $category_defect_types = FixMyStreet::App->model('DB::DefectType')->by_categories($c->cobrand->council_area_id, @contacts);
+ $c->stash->{category_defect_types} = $category_defect_types;
+ }
+
my %seen;
foreach my $contact (@contacts) {
$bodies_to_list{ $contact->body_id } = $contact->body;
unless ( $seen{$contact->category} ) {
- push @category_options, $contact->category;
+ push @category_options, { name => $contact->category, value => $contact->category_display };
my $metas = $contact->get_metadata_for_input;
$category_extras{$contact->category} = $metas if @$metas;
@@ -650,13 +665,15 @@ sub setup_categories_and_bodies : Private {
$non_public_categories{ $contact->category } = 1 if $contact->non_public;
}
- $seen{$contact->category} = 1;
+ $seen{$contact->category} = $contact->category_display;
}
if (@category_options) {
# If there's an Other category present, put it at the bottom
- @category_options = ( _('-- Pick a category --'), grep { $_ ne _('Other') } @category_options );
- push @category_options, _('Other') if $seen{_('Other')};
+ @category_options = (
+ { name => _('-- Pick a category --'), value => _('-- Pick a category --') },
+ grep { $_->{name} ne _('Other') } @category_options );
+ push @category_options, { name => _('Other'), value => $seen{_('Other')} } if $seen{_('Other')};
}
$c->cobrand->call_hook(munge_category_list => \@category_options, \@contacts, \%category_extras);
@@ -679,6 +696,15 @@ sub setup_categories_and_bodies : Private {
$c->stash->{missing_details_body_names} = \@missing_details_body_names;
}
+sub setup_report_extra_fields : Private {
+ my ( $self, $c ) = @_;
+
+ 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;
+ $c->stash->{report_extra_fields} = \@extras;
+}
+
=head2 check_form_submitted
$bool = $c->forward('check_form_submitted');
@@ -734,7 +760,8 @@ sub process_user : Private {
$user->title( $user_title ) if $user_title;
$report->user( $user );
- if ($c->stash->{contributing_as_body} = $user->contributing_as('body', $c, $c->stash->{bodies})) {
+ if ($c->stash->{contributing_as_body} = $user->contributing_as('body', $c, $c->stash->{bodies}) or
+ $c->stash->{contributing_as_anonymous_user} = $user->contributing_as('anonymous_user', $c, $c->stash->{bodies})) {
$report->name($user->from_body->name);
$user->name($user->from_body->name) unless $user->name;
$c->stash->{no_reporter_alert} = 1;
@@ -814,6 +841,8 @@ sub process_report : Private {
# set some simple bool values (note they get inverted)
if ($c->stash->{contributing_as_body}) {
$report->anonymous(0);
+ } elsif ($c->stash->{contributing_as_anonymous_user}) {
+ $report->anonymous(1);
} else {
$report->anonymous( $params{may_show_name} ? 0 : 1 );
}
@@ -933,7 +962,24 @@ sub set_report_extras : Private {
}
}
- $c->cobrand->process_open311_extras( $c, @$contacts[0]->body_id, \@extra )
+ foreach my $extra_fields (@{ $c->stash->{report_extra_fields} }) {
+ my $metas = $extra_fields->get_extra_fields;
+ $param_prefix = "extra[" . $extra_fields->id . "]";
+ foreach my $field ( @$metas ) {
+ if ( lc( $field->{required} ) eq 'true' && !$c->cobrand->category_extra_hidden($field->{code})) {
+ 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}) || '',
+ };
+ }
+ }
+
+ $c->cobrand->process_open311_extras( $c, @$contacts[0]->body, \@extra )
if ( scalar @$contacts );
if ( @extra ) {
@@ -1140,7 +1186,7 @@ sub save_user_and_report : Private {
sub created_as_someone_else : Private {
my ($self, $c, $bodies) = @_;
- return $c->stash->{contributing_as_another_user} || $c->stash->{contributing_as_body};
+ return $c->stash->{contributing_as_another_user} || $c->stash->{contributing_as_body} || $c->stash->{contributing_as_anonymous_user};
}
=head2 generate_map
@@ -1203,9 +1249,14 @@ sub redirect_or_confirm_creation : Private {
to => [ [ $report->user->email, $report->name ] ],
} );
}
- $c->log->info($report->user->id . ' was logged in, showing confirmation page for ' . $report->id);
- $c->stash->{created_report} = 'loggedin';
- $c->stash->{template} = 'tokens/confirm_problem.html';
+ if ($c->user_exists && $c->user->has_body_permission_to('planned_reports')) {
+ $c->log->info($report->user->id . ' is an inspector - redirecting straight to report page for ' . $report->id);
+ $c->res->redirect( '/report/'. $report->id );
+ } else {
+ $c->log->info($report->user->id . ' was logged in, showing confirmation page for ' . $report->id);
+ $c->stash->{created_report} = 'loggedin';
+ $c->stash->{template} = 'tokens/confirm_problem.html';
+ }
return 1;
}
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index 4c2d92d5e..033f5c017 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -128,7 +128,7 @@ sub process_user : Private {
$update->user( $user );
# Just in case, make sure the user will have a name
- if ($c->stash->{contributing_as_body}) {
+ if ($c->stash->{contributing_as_body} or $c->stash->{contributing_as_anonymous_user}) {
$user->name($user->from_body->name) unless $user->name;
}
@@ -277,17 +277,19 @@ sub process_update : Private {
$update->mark_open($params{reopen} ? 1 : 0);
$c->stash->{contributing_as_body} = $c->user_exists && $c->user->contributing_as('body', $c, $update->problem->bodies_str_ids);
+ $c->stash->{contributing_as_anonymous_user} = $c->user_exists && $c->user->contributing_as('anonymous_user', $c, $update->problem->bodies_str_ids);
if ($c->stash->{contributing_as_body}) {
$update->name($c->user->from_body->name);
$update->anonymous(0);
+ } elsif ($c->stash->{contributing_as_anonymous_user}) {
+ $update->name($c->user->from_body->name);
+ $update->anonymous(1);
} else {
$update->name($name);
$update->anonymous($c->get_param('may_show_name') ? 0 : 1);
}
if ( $params{state} ) {
- $params{state} = 'fixed - council'
- if $params{state} eq 'fixed' && $c->user && $c->user->belongs_to_body( $update->problem->bodies_str );
$update->problem_state( $params{state} );
} else {
# we do this so we have a record of the state of the problem at this point
@@ -309,7 +311,8 @@ sub process_update : Private {
my @extra; # Next function fills this, but we don't need it here.
# This is just so that the error checking for these extra fields runs.
# TODO Use extra here as it is used on reports.
- $c->cobrand->process_open311_extras( $c, $update->problem->bodies_str, \@extra );
+ my $body = (values %{$update->problem->bodies})[0];
+ $c->cobrand->process_open311_extras( $c, $body, \@extra );
if ( $c->get_param('fms_extra_title') ) {
my %extras = ();
@@ -344,14 +347,11 @@ sub check_for_errors : Private {
my ( $self, $c ) = @_;
# they have to be an authority user to update the state
- if ( $c->get_param('state') ) {
+ 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 );
-
- my $state = $c->get_param('state');
- $state = 'fixed - council' if $state eq 'fixed';
- $error = 1 unless ( grep { $state eq $_ } ( FixMyStreet::DB::Result::Problem->council_states() ) );
-
+ $error = 1 unless grep { $state eq $_ } FixMyStreet::DB::Result::Problem->visible_states();
if ( $error ) {
$c->stash->{errors} ||= [];
push @{ $c->stash->{errors} }, _('There was a problem with your update. Please try again.');
@@ -548,24 +548,17 @@ sub signup_for_alerts : Private {
my ( $self, $c ) = @_;
my $update = $c->stash->{update};
+ my $user = $update->user;
+ my $problem_id = $update->problem_id;
+
if ( $c->stash->{add_alert} ) {
my $options = {
- user => $update->user,
- alert_type => 'new_updates',
- parameter => $update->problem_id,
+ cobrand => $update->cobrand,
+ cobrand_data => $update->cobrand_data,
+ lang => $update->lang,
};
- my $alert = $c->model('DB::Alert')->find($options);
- unless ($alert) {
- $alert = $c->model('DB::Alert')->create({
- %$options,
- cobrand => $update->cobrand,
- cobrand_data => $update->cobrand_data,
- lang => $update->lang,
- });
- }
- $alert->confirm();
-
- } elsif ( my $alert = $update->user->alert_for_problem($update->problem_id) ) {
+ $user->create_alert($problem_id, $options);
+ } elsif ( my $alert = $user->alert_for_problem($problem_id) ) {
$alert->disable();
}
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
index ed851f71f..8f8205719 100644
--- a/perllib/FixMyStreet/App/Controller/Reports.pm
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -36,6 +36,12 @@ sub index : Path : Args(0) {
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(
@@ -54,6 +60,15 @@ 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;
+ }
+ }
+
# Fetch all bodies
my @bodies = $c->model('DB::Body')->search({
deleted => 0,
@@ -67,15 +82,21 @@ sub index : Path : Args(0) {
$c->stash->{bodies} = \@bodies;
$c->stash->{any_empty_bodies} = any { $_->get_column('area_count') == 0 } @bodies;
- eval {
+ my $dashboard = eval {
+ my $data = File::Slurp::read_file(
+ FixMyStreet->path_to( '../data/all-reports-dashboard.json' )->stringify
+ );
+ $c->stash(decode_json($data));
+ return 1;
+ };
+ my $table = eval {
my $data = File::Slurp::read_file(
FixMyStreet->path_to( '../data/all-reports.json' )->stringify
);
- my $j = decode_json($data);
- $c->stash->{fixed} = $j->{fixed};
- $c->stash->{open} = $j->{open};
+ $c->stash(decode_json($data));
+ return 1;
};
- if ($@) {
+ if (!$dashboard && !$table) {
my $message = _("There was a problem showing the All Reports page. Please try again later.");
if ($c->config->{STAGING_SITE}) {
$message .= '</p><p>Perhaps the bin/update-all-reports script needs running. Use: bin/update-all-reports</p><p>'
@@ -88,7 +109,7 @@ sub index : Path : Args(0) {
$c->response->header('Cache-Control' => 'max-age=3600');
}
-=head2 index
+=head2 body
Show the summary page for a particular body.
@@ -99,7 +120,7 @@ sub body : Path : Args(1) {
$c->detach( 'ward', [ $body ] );
}
-=head2 index
+=head2 ward
Show the summary page for a particular ward.
@@ -135,7 +156,7 @@ sub ward : Path : Args(2) {
distinct => 1,
order_by => [ 'category' ],
} )->all;
- @categories = map { $_->category } @categories;
+ @categories = map { { name => $_->category, value => $_->category_display } } @categories;
$c->stash->{filter_categories} = \@categories;
$c->stash->{filter_category} = { map { $_ => 1 } $c->get_param_list('filter_category', 1) };
@@ -303,6 +324,19 @@ sub body_check : Private {
}
}
+ my @translations = $c->model('DB::Translation')->search( {
+ tbl => 'body',
+ col => 'name',
+ msgstr => $q_body
+ } )->all;
+
+ if (@translations == 1) {
+ if ( my $body = $c->model('DB::Body')->find( { id => $translations[0]->object_id } ) ) {
+ $c->stash->{body} = $body;
+ return;
+ }
+ }
+
# No result, bad body name.
$c->detach( 'redirect_index' );
}
@@ -369,8 +403,6 @@ sub load_and_group_problems : Private {
$c->forward('stash_report_sort', [ $c->cobrand->reports_ordering ]);
my $page = $c->get_param('p') || 1;
- # NB: If 't' is specified, it will override 'status'.
- my $type = $c->get_param('t') || 'all';
my $category = [ $c->get_param_list('filter_category', 1) ];
my $states = $c->stash->{filter_problem_states};
@@ -382,6 +414,18 @@ sub load_and_group_problems : Private {
order_by => $c->stash->{sort_order},
rows => $c->cobrand->reports_per_page,
};
+ if ($c->user_exists && $c->stash->{body}) {
+ my $bid = $c->stash->{body}->id;
+ my $prefetch = [];
+ if ($c->user->has_permission_to('planned_reports', $bid)) {
+ push @$prefetch, 'user_planned_reports';
+ }
+ if ($c->user->has_permission_to('report_edit_priority', $bid) || $c->user->has_permission_to('report_inspect', $bid)) {
+ push @$prefetch, 'response_priority';
+ }
+ $prefetch = $prefetch->[0] if @$prefetch == 1;
+ $filter->{prefetch} = $prefetch;
+ }
if (defined $c->stash->{filter_status}{shortlisted}) {
$where->{'me.id'} = { '=', \"user_planned_reports.report_id"};
@@ -398,25 +442,6 @@ sub load_and_group_problems : Private {
$where->{'me.id'} = { -not_in => $shortlisted_ids };
}
- my $not_open = [ FixMyStreet::DB::Result::Problem::fixed_states(), FixMyStreet::DB::Result::Problem::closed_states() ];
- if ( $type eq 'new' ) {
- $where->{confirmed} = { '>', \"current_timestamp - INTERVAL '4 week'" };
- $where->{state} = { 'IN', [ FixMyStreet::DB::Result::Problem::open_states() ] };
- } elsif ( $type eq 'older' ) {
- $where->{confirmed} = { '<', \"current_timestamp - INTERVAL '4 week'" };
- $where->{lastupdate} = { '>', \"current_timestamp - INTERVAL '8 week'" };
- $where->{state} = { 'IN', [ FixMyStreet::DB::Result::Problem::open_states() ] };
- } elsif ( $type eq 'unknown' ) {
- $where->{lastupdate} = { '<', \"current_timestamp - INTERVAL '8 week'" };
- $where->{state} = { 'IN', [ FixMyStreet::DB::Result::Problem::open_states() ] };
- } elsif ( $type eq 'fixed' ) {
- $where->{lastupdate} = { '>', \"current_timestamp - INTERVAL '8 week'" };
- $where->{state} = $not_open;
- } elsif ( $type eq 'older_fixed' ) {
- $where->{lastupdate} = { '<', \"current_timestamp - INTERVAL '8 week'" };
- $where->{state} = $not_open;
- }
-
if (@$category) {
$where->{category} = $category;
}
@@ -445,7 +470,6 @@ sub load_and_group_problems : Private {
my ( %problems, @pins );
while ( my $problem = $problems->next ) {
- $c->log->debug( $problem->cobrand . ', cobrand is ' . $c->cobrand->moniker );
if ( !$c->stash->{body} ) {
add_row( $c, $problem, 0, \%problems, \@pins );
next;
@@ -531,6 +555,18 @@ sub stash_report_filter_status : Private {
$filter_status{unshortlisted} = 1;
}
+ if ($c->user and ($c->user->is_superuser or (
+ $c->stash->{body} and $c->user->belongs_to_body($c->stash->{body}->id)
+ ))) {
+ $c->stash->{filter_states} = $c->cobrand->state_groups_inspect;
+ foreach my $state (FixMyStreet::DB::Result::Problem->visible_states()) {
+ if ($status{$state}) {
+ $filter_problem_states{$state} = 1;
+ $filter_status{$state} = 1;
+ }
+ }
+ }
+
if (keys %filter_problem_states == 0) {
my $s = FixMyStreet::DB::Result::Problem->open_states();
%filter_problem_states = (%filter_problem_states, %$s);
diff --git a/perllib/FixMyStreet/App/Controller/Root.pm b/perllib/FixMyStreet/App/Controller/Root.pm
index 4f098dfc3..7f70623ae 100644
--- a/perllib/FixMyStreet/App/Controller/Root.pm
+++ b/perllib/FixMyStreet/App/Controller/Root.pm
@@ -16,6 +16,18 @@ FixMyStreet::App::Controller::Root - Root Controller for FixMyStreet::App
=head1 METHODS
+=head2 begin
+
+Any pre-flight checking for all requests
+
+=cut
+sub begin : Private {
+ my ( $self, $c ) = @_;
+
+ $c->forward( 'check_login_required' );
+}
+
+
=head2 auto
Set up general things for this instance
@@ -58,6 +70,11 @@ sub index : Path : Args(0) {
return;
}
+ if ($c->stash->{homepage_template}) {
+ $c->stash->{template} = $c->stash->{homepage_template};
+ $c->detach;
+ }
+
$c->forward('/auth/get_csrf_token');
}
@@ -125,6 +142,27 @@ sub page_error : Private {
$c->response->status($code);
}
+sub check_login_required : Private {
+ my ($self, $c) = @_;
+
+ return if $c->user_exists || !FixMyStreet->config('LOGIN_REQUIRED');
+
+ # Whitelisted URL patterns are allowed without login
+ my $whitelist = qr{
+ ^auth(/|$)
+ | ^js/translation_strings\.(.*?)\.js
+ | ^[PACQM]/ # various tokens that log the user in
+ }x;
+ return if $c->request->path =~ $whitelist;
+
+ # Blacklisted URLs immediately 404
+ # This is primarily to work around a Safari bug where the appcache
+ # URL is requested in an infinite loop if it returns a 302 redirect.
+ $c->detach('/page_error_404_not_found', []) if $c->request->path =~ /^offline/;
+
+ $c->detach( '/auth/redirect' );
+}
+
=head2 end
Attempt to render a view, if needed.
diff --git a/perllib/FixMyStreet/App/Controller/Rss.pm b/perllib/FixMyStreet/App/Controller/Rss.pm
index 28f1aba43..3497ad0e1 100755
--- a/perllib/FixMyStreet/App/Controller/Rss.pm
+++ b/perllib/FixMyStreet/App/Controller/Rss.pm
@@ -3,6 +3,7 @@ package FixMyStreet::App::Controller::Rss;
use Moose;
use namespace::autoclean;
use POSIX qw(strftime);
+use HTML::Entities;
use URI::Escape;
use XML::RSS;
@@ -11,8 +12,7 @@ use FixMyStreet::App::Model::PhotoSet;
use FixMyStreet::Gaze;
use mySociety::Locale;
use mySociety::MaPit;
-use mySociety::Sundries qw(ordinal);
-use mySociety::Web qw(ent);
+use Lingua::EN::Inflect qw(ORD);
BEGIN { extends 'Catalyst::Controller'; }
@@ -250,7 +250,7 @@ sub add_row : Private {
};
$row->{created} = strftime("%e %B", $6, $5, $4, $3, $2-1, $1-1900, -1, -1, 0);
$row->{created} =~ s/^\s+//;
- $row->{created} =~ s/^(\d+)/ordinal($1)/e if $c->stash->{lang_code} eq 'en-gb';
+ $row->{created} =~ s/^(\d+)/ORD($1)/e if $c->stash->{lang_code} eq 'en-gb';
}
if ($row->{confirmed}) {
$row->{confirmed} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/;
@@ -259,7 +259,7 @@ sub add_row : Private {
};
$row->{confirmed} = strftime("%e %B", $6, $5, $4, $3, $2-1, $1-1900, -1, -1, 0);
$row->{confirmed} =~ s/^\s+//;
- $row->{confirmed} =~ s/^(\d+)/ordinal($1)/e if $c->stash->{lang_code} eq 'en-gb';
+ $row->{confirmed} =~ s/^(\d+)/ORD($1)/e if $c->stash->{lang_code} eq 'en-gb';
}
(my $title = _($alert_type->item_title)) =~ s/\{\{(.*?)}}/$row->{$1}/g;
@@ -270,13 +270,13 @@ sub add_row : Private {
my $url = $base_url . $link;
my %item = (
- title => ent($title),
+ title => encode_entities($title),
link => $url,
guid => $url,
- description => ent(ent($desc)) # Yes, double-encoded, really.
+ description => encode_entities(encode_entities($desc)) # Yes, double-encoded, really.
);
$item{pubDate} = $pubDate if $pubDate;
- $item{category} = ent($row->{category}) if $row->{category};
+ $item{category} = encode_entities($row->{category}) if $row->{category};
if ($c->cobrand->allow_photo_display($row) && $row->{photo}) {
# Bit yucky as we don't have full objects here
@@ -285,16 +285,16 @@ sub add_row : Private {
my ($hash, $format) = split /\./, $first_fn;
my $cachebust = substr($hash, 0, 8);
my $key = $alert_type->item_table eq 'comment' ? 'c/' : '';
- $item{description} .= ent("\n<br><img src=\"". $base_url . "/photo/$key$row->{id}.0.$format?$cachebust\">");
+ $item{description} .= encode_entities("\n<br><img src=\"". $base_url . "/photo/$key$row->{id}.0.$format?$cachebust\">");
}
if ( $row->{used_map} ) {
- my $address = $c->cobrand->find_closest_address_for_rss( $row->{latitude}, $row->{longitude}, $row );
- $item{description} .= ent("\n<br>$address") if $address;
+ my $address = $c->cobrand->find_closest_address_for_rss($row);
+ $item{description} .= encode_entities("\n<br>$address") if $address;
}
my $recipient_name = $c->cobrand->contact_name;
- $item{description} .= ent("\n<br><a href='$url'>" .
+ $item{description} .= encode_entities("\n<br><a href='$url'>" .
sprintf(_("Report on %s"), $recipient_name) . "</a>");
if ($row->{latitude} || $row->{longitude}) {
@@ -329,9 +329,9 @@ sub add_parameters : Private {
(my $desc = _($alert_type->head_description)) =~ s/\{\{(.*?)}}/$row->{$1}/g;
$c->stash->{rss}->channel(
- title => ent($title),
+ title => encode_entities($title),
link => $c->uri_for($link) . ($c->stash->{qs} || ''),
- description => ent($desc),
+ description => encode_entities($desc),
language => 'en-gb',
);
}
diff --git a/perllib/FixMyStreet/App/Model/DB.pm b/perllib/FixMyStreet/App/Model/DB.pm
index ac1f98dc9..c116abffc 100644
--- a/perllib/FixMyStreet/App/Model/DB.pm
+++ b/perllib/FixMyStreet/App/Model/DB.pm
@@ -5,11 +5,26 @@ use strict;
use warnings;
use FixMyStreet;
+use Catalyst::Utils;
+use Moose;
+
+with 'Catalyst::Component::InstancePerContext';
__PACKAGE__->config(
- schema_class => 'FixMyStreet::DB',
- connect_info => sub { FixMyStreet::DB->storage->dbh },
+ schema_class => 'FixMyStreet::DB::Schema',
+ connect_info => sub { FixMyStreet::DB->schema->storage->dbh },
);
+__PACKAGE__->config(
+ traits => ['QueryLog::AdoptPlack'],
+)
+ if Catalyst::Utils::env_value( 'FixMyStreet::App', 'DEBUG' );
+
+sub build_per_context_instance {
+ my ( $self, $c ) = @_;
+ # $self->schema->cobrand($c->cobrand);
+ $self->schema->cache({});
+ return $self;
+}
=head1 NAME
@@ -17,7 +32,7 @@ FixMyStreet::App::Model::DB - Catalyst DBIC Schema Model
=head1 DESCRIPTION
-L<Catalyst::Model::DBIC::Schema> Model using schema L<FixMyStreet::DB>
+L<Catalyst::Model::DBIC::Schema> Model using schema L<FixMyStreet::DB::Schema>
=cut
diff --git a/perllib/FixMyStreet/App/View/Web.pm b/perllib/FixMyStreet/App/View/Web.pm
index 496463700..93aa0e2fb 100644
--- a/perllib/FixMyStreet/App/View/Web.pm
+++ b/perllib/FixMyStreet/App/View/Web.pm
@@ -18,6 +18,7 @@ __PACKAGE__->config(
expose_methods => [
'tprintf', 'prettify_dt',
'version', 'decode',
+ 'prettify_state',
],
FILTERS => {
add_links => \&add_links,
@@ -167,5 +168,11 @@ sub decode {
return $text;
}
+sub prettify_state {
+ my ($self, $c, $text, $single_fixed) = @_;
+
+ return FixMyStreet::DB->resultset("State")->display($text, $single_fixed);
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand.pm b/perllib/FixMyStreet/Cobrand.pm
index 4b9f2bd0b..a0a076f67 100644
--- a/perllib/FixMyStreet/Cobrand.pm
+++ b/perllib/FixMyStreet/Cobrand.pm
@@ -158,8 +158,8 @@ sub body_handler {
foreach my $avail ( $class->available_cobrand_classes ) {
my $cobrand = $class->get_class_for_moniker($avail->{moniker})->new({});
- next unless $cobrand->can('council_id');
- return $cobrand if $areas->{$cobrand->council_id};
+ next unless $cobrand->can('council_area_id');
+ return $cobrand if $areas->{$cobrand->council_area_id};
}
}
diff --git a/perllib/FixMyStreet/Cobrand/Angus.pm b/perllib/FixMyStreet/Cobrand/Angus.pm
index 0361c2d11..51a3da56a 100644
--- a/perllib/FixMyStreet/Cobrand/Angus.pm
+++ b/perllib/FixMyStreet/Cobrand/Angus.pm
@@ -4,7 +4,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2550; }
+sub council_area_id { return 2550; }
sub council_area { return 'Angus'; }
sub council_name { return 'Angus Council'; }
sub council_url { return 'angus'; }
@@ -70,13 +70,17 @@ sub temp_update_contacts {
my $contact_rs = $self->{c}->model('DB::Contact');
+ my $body = FixMyStreet::DB->resultset('Body')->search({
+ 'body_areas.area_id' => $self->council_area_id,
+ }, { join => 'body_areas' })->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_id => $self->council_id,
+ body => $body,
category => $category,
%{ $category_details || {} },
},
diff --git a/perllib/FixMyStreet/Cobrand/Borsetshire.pm b/perllib/FixMyStreet/Cobrand/Borsetshire.pm
new file mode 100644
index 000000000..7ddcff469
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Borsetshire.pm
@@ -0,0 +1,32 @@
+package FixMyStreet::Cobrand::Borsetshire;
+use parent 'FixMyStreet::Cobrand::Whitelabel';
+
+use strict;
+use warnings;
+
+sub council_area_id { return 2608; }
+sub council_area { return 'Borsetshire'; }
+sub council_name { return 'Borsetshire County Council'; }
+sub council_url { return 'demo'; }
+
+sub example_places {
+ return ( 'BS36 2NS', 'Coalpit Heath' );
+}
+
+sub pin_colour {
+ my ( $self, $p, $context ) = @_;
+ return 'grey' if $p->is_closed;
+ return 'green' if $p->is_fixed;
+ return 'yellow' if $p->state eq 'confirmed';
+ return 'orange'; # all the other `open_states` like "in progress"
+}
+
+sub path_to_pin_icons {
+ return '/cobrands/oxfordshire/images/';
+}
+
+sub send_questionnaires {
+ return 0;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/Bristol.pm b/perllib/FixMyStreet/Cobrand/Bristol.pm
index fa7f98666..b11a52643 100644
--- a/perllib/FixMyStreet/Cobrand/Bristol.pm
+++ b/perllib/FixMyStreet/Cobrand/Bristol.pm
@@ -4,7 +4,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2561; }
+sub council_area_id { return 2561; }
sub council_area { return 'Bristol'; }
sub council_name { return 'Bristol County Council'; }
sub council_url { return 'bristol'; }
@@ -68,7 +68,7 @@ sub categories_restriction {
# cobrand, not the email categories from FMS.com. We've set up the
# Email categories with a devolved send_method, so can identify Open311
# categories as those which have a blank send_method.
- return $rs->search( { send_method => undef } );
+ return $rs->search( { 'me.send_method' => undef } );
}
1;
diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm
index 169175947..e7d5e186a 100644
--- a/perllib/FixMyStreet/Cobrand/Bromley.pm
+++ b/perllib/FixMyStreet/Cobrand/Bromley.pm
@@ -5,7 +5,7 @@ use strict;
use warnings;
use DateTime::Format::W3CDTF;
-sub council_id { return 2482; }
+sub council_area_id { return 2482; }
sub council_area { return 'Bromley'; }
sub council_name { return 'Bromley Council'; }
sub council_url { return 'bromley'; }
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index ac70fff08..250919d09 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -4,6 +4,7 @@ use base 'FixMyStreet::Cobrand::Base';
use strict;
use warnings;
use FixMyStreet;
+use FixMyStreet::DB;
use FixMyStreet::Geocode::Bing;
use DateTime;
use Encode;
@@ -71,7 +72,7 @@ a cobrand that only wants some of the data.
sub problems {
my $self = shift;
- return $self->problems_restriction($self->{c}->model('DB::Problem'));
+ return $self->problems_restriction(FixMyStreet::DB->resultset('Problem'));
}
=head1 problems_on_map
@@ -83,7 +84,7 @@ restricted to a subset if we're on a cobrand that only wants some of the data.
sub problems_on_map {
my $self = shift;
- return $self->problems_on_map_restriction($self->{c}->model('DB::Problem'));
+ return $self->problems_on_map_restriction(FixMyStreet::DB->resultset('Problem'));
}
=head1 updates
@@ -95,7 +96,7 @@ a cobrand that only wants some of the data.
sub updates {
my $self = shift;
- return $self->updates_restriction($self->{c}->model('DB::Comment'));
+ return $self->updates_restriction(FixMyStreet::DB->resultset('Comment'));
}
=head1 problems_restriction/updates_restriction
@@ -149,7 +150,7 @@ a cobrand that only wants some of the data.
sub users {
my $self = shift;
- return $self->users_restriction($self->{c}->model('DB::User'));
+ return $self->users_restriction(FixMyStreet::DB->resultset('User'));
}
=head1 users_restriction
@@ -178,7 +179,7 @@ sub restriction {
return $self->moniker ? { cobrand => $self->moniker } : {};
}
-=head2 base_url_with_lang
+=head2 base_url_with_lang
=cut
@@ -268,6 +269,8 @@ sub set_lang_and_domain {
DateTime->DefaultLocale( 'en_US' );
}
+ FixMyStreet::DB->schema->lang($set_lang);
+
return $set_lang;
}
sub languages { FixMyStreet->config('LANGUAGES') || [] }
@@ -355,7 +358,7 @@ sub front_stats_data {
Returns any disambiguating information available. Defaults to none.
-=cut
+=cut
sub disambiguate_location { FixMyStreet->config('GEOCODING_DISAMBIGUATION') or {}; }
@@ -509,27 +512,35 @@ sub geocoded_string_check { return 1; }
=head2 find_closest
-Used by send-reports to attach nearest things to the bottom of the report
+Used by send-reports and similar to attach nearest things to the bottom of the
+report.
=cut
sub find_closest {
- my ( $self, $latitude, $longitude, $problem ) = @_;
- my $str = '';
+ my ( $self, $problem, $as_data ) = @_;
- if ( my $j = FixMyStreet::Geocode::Bing::reverse( $latitude, $longitude, disambiguate_location()->{bing_culture} ) ) {
+ my $j = $problem->geocode;
+ if (!$j) {
+ $j = FixMyStreet::Geocode::Bing::reverse( $problem->latitude, $problem->longitude,
+ disambiguate_location()->{bing_culture} );
# cache the bing results for use in alerts
- if ( $problem ) {
- $problem->geocode( $j );
- $problem->update;
- }
- if ($j->{resourceSets}[0]{resources}[0]{name}) {
- $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"),
- $j->{resourceSets}[0]{resources}[0]{name}) . "\n\n";
+ $problem->geocode( $j );
+ $problem->update;
+ }
+
+ my $data = $as_data ? {} : '';
+ if ($j && $j->{resourceSets}[0]{resources}[0]{name}) {
+ my $str = $j->{resourceSets}[0]{resources}[0]{name};
+ if ($as_data) {
+ $data->{road} = $str;
+ } else {
+ $data .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"),
+ $str) . "\n\n";
}
}
- return $str;
+ return $data;
}
=head2 find_closest_address_for_rss
@@ -539,26 +550,14 @@ Used by rss feeds to provide a bit more context
=cut
sub find_closest_address_for_rss {
- my ( $self, $latitude, $longitude, $problem ) = @_;
- my $str = '';
+ my ( $self, $problem ) = @_;
- my $j;
- if ( $problem && ref($problem) =~ /FixMyStreet/ && $problem->can( 'geocode' ) ) {
- $j = $problem->geocode;
- } else {
+ if (ref($problem) eq 'HASH') {
$problem = FixMyStreet::App->model('DB::Problem')->find( { id => $problem->{id} } );
- $j = $problem->geocode;
}
+ my $j = $problem->geocode;
- # if we've not cached it then we don't want to look it up in order to avoid
- # hammering the bing api
- # if ( !$j ) {
- # $j = FixMyStreet::Geocode::Bing::reverse( $latitude, $longitude, disambiguate_location()->{bing_culture}, 1 );
-
- # $problem->geocode( $j );
- # $problem->update;
- # }
-
+ my $str = '';
if ($j && $j->{resourceSets}[0]{resources}[0]{name}) {
my $address = $j->{resourceSets}[0]{resources}[0]{address};
my @address;
@@ -643,6 +642,7 @@ sub admin_pages {
# There are some pages that only super users can see
if ( $user->is_superuser ) {
$pages->{flagged} = [ _('Flagged'), 7 ];
+ $pages->{states} = [ _('States'), 8 ];
$pages->{config} = [ _('Configuration'), 9];
};
# And some that need special permissions
@@ -669,6 +669,10 @@ sub admin_pages {
$pages->{users} = [ _('Users'), 6 ];
$pages->{user_edit} = [ undef, undef ];
}
+ if ( $self->allow_report_extra_fields && $user->has_body_permission_to('category_edit') ) {
+ $pages->{reportextrafields} = [ _('Extra Fields'), 10 ];
+ $pages->{reportextrafields_edit} = [ undef, undef ];
+ }
return $pages;
}
@@ -711,6 +715,7 @@ sub available_permissions {
report_instruct => _("Instruct contractors to fix problems"), # future use
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"),
view_body_contribute_details => _("See user detail for reports created as the council"),
@@ -816,7 +821,7 @@ sub is_two_tier { 0; }
=item council_rss_alert_options
-Generate a set of options for council rss alerts.
+Generate a set of options for council rss alerts.
=cut
@@ -948,6 +953,15 @@ sub pin_colour {
return $p->is_fixed ? 'green' : 'red';
}
+=head2 pin_new_report_colour
+
+Returns the colour of pin to be used for a new report.
+
+=cut
+sub pin_new_report_colour {
+ return 'green';
+}
+
=head2 path_to_pin_icons
Used to override the path for the pin icons if you want to add custom pin icons
@@ -1053,6 +1067,28 @@ sub show_unconfirmed_reports {
0;
}
+sub state_groups_admin {
+ my $rs = FixMyStreet::DB->resultset("State");
+ my @fixed = FixMyStreet::DB::Result::Problem->fixed_states;
+ [
+ [ $rs->display('confirmed'), [ FixMyStreet::DB::Result::Problem->open_states ] ],
+ @fixed ? [ $rs->display('fixed'), [ FixMyStreet::DB::Result::Problem->fixed_states ] ] : (),
+ [ $rs->display('closed'), [ FixMyStreet::DB::Result::Problem->closed_states ] ],
+ [ $rs->display('hidden'), [ FixMyStreet::DB::Result::Problem->hidden_states ] ]
+ ]
+}
+
+sub state_groups_inspect {
+ my $rs = FixMyStreet::DB->resultset("State");
+ my @fixed = FixMyStreet::DB::Result::Problem->fixed_states;
+ [
+ [ $rs->display('confirmed'), [ grep { $_ ne 'planned' } FixMyStreet::DB::Result::Problem->open_states ] ],
+ @fixed ? [ $rs->display('fixed'), [ 'fixed - council' ] ] : (),
+ [ $rs->display('closed'), [ grep { $_ ne 'closed' } FixMyStreet::DB::Result::Problem->closed_states ] ],
+ [ $rs->display('hidden'), [ 'hidden' ] ]
+ ]
+}
+
=head2 never_confirm_updates
If true then we never send an email to confirm an update
@@ -1212,5 +1248,16 @@ the 'n days ago' format is used. By default the absolute date is always used.
=cut
sub display_days_ago_threshold { 0 }
+=head2 allow_report_extra_fields
+
+Used to control whether site-wide extra fields are available. If true,
+users with the category_edit permission can add site-wide fields via the
+admin.
+
+=cut
+
+sub allow_report_extra_fields { 0 }
+
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/EastHerts.pm b/perllib/FixMyStreet/Cobrand/EastHerts.pm
index ea5ed7f55..0e60c6b08 100644
--- a/perllib/FixMyStreet/Cobrand/EastHerts.pm
+++ b/perllib/FixMyStreet/Cobrand/EastHerts.pm
@@ -4,7 +4,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2342; }
+sub council_area_id { return 2342; }
sub council_area { return 'East Hertfordshire'; }
sub council_name { return 'East Hertfordshire District Council'; }
sub council_url { return 'eastherts'; }
@@ -51,4 +51,4 @@ sub contact_email {
return join( '@', 'enquiries', 'eastherts.gov.uk' );
}
-1; \ No newline at end of file
+1;
diff --git a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
index fd788d892..ddae3010b 100644
--- a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
+++ b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
@@ -68,8 +68,8 @@ sub geocoded_string_check {
}
sub find_closest {
- my ( $self, $latitude, $longitude ) = @_;
- return FixMyStreet::Geocode::OSM::closest_road_text( $self, $latitude, $longitude );
+ my ( $self, $problem ) = @_;
+ return FixMyStreet::Geocode::OSM::closest_road_text( $self, $problem->latitude, $problem->longitude );
}
# Used by send-reports, calling find_closest, calling OSM geocoding
diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
index 1052bac0e..c50721334 100644
--- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
+++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
@@ -21,6 +21,8 @@ sub path_to_email_templates {
sub add_response_headers {
my $self = shift;
+ # uncoverable branch true
+ return if $self->{c}->debug;
my $csp_nonce = $self->{c}->stash->{csp_nonce} = unpack('h*', mySociety::Random::random_bytes(16, 1));
$self->{c}->res->header('Content-Security-Policy', "script-src 'self' www.google-analytics.com www.googleadservices.com 'unsafe-inline' 'nonce-$csp_nonce'")
}
diff --git a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm
index 324811008..07a4ef920 100644
--- a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm
+++ b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm
@@ -67,8 +67,8 @@ sub geocoded_string_check {
}
sub find_closest {
- my ( $self, $latitude, $longitude ) = @_;
- return FixMyStreet::Geocode::OSM::closest_road_text( $self, $latitude, $longitude );
+ my ( $self, $problem ) = @_;
+ return FixMyStreet::Geocode::OSM::closest_road_text( $self, $problem->latitude, $problem->longitude );
}
# Used by send-reports, calling find_closest, calling OSM geocoding
diff --git a/perllib/FixMyStreet/Cobrand/Greenwich.pm b/perllib/FixMyStreet/Cobrand/Greenwich.pm
index 700a12782..ce4fae381 100644
--- a/perllib/FixMyStreet/Cobrand/Greenwich.pm
+++ b/perllib/FixMyStreet/Cobrand/Greenwich.pm
@@ -4,7 +4,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2493; }
+sub council_area_id { return 2493; }
sub council_area { return 'Greenwich'; }
sub council_name { return 'Royal Borough of Greenwich'; }
sub council_url { return 'greenwich'; }
diff --git a/perllib/FixMyStreet/Cobrand/Hart.pm b/perllib/FixMyStreet/Cobrand/Hart.pm
index 42c4a636e..3ff2a2a19 100644
--- a/perllib/FixMyStreet/Cobrand/Hart.pm
+++ b/perllib/FixMyStreet/Cobrand/Hart.pm
@@ -4,7 +4,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2333; } # http://mapit.mysociety.org/area/2333.html
+sub council_area_id { return 2333; } # http://mapit.mysociety.org/area/2333.html
sub council_area { return 'Hart'; }
sub council_name { return 'Hart Council'; }
sub council_url { return 'hart'; }
diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
index 3e262a700..44747a16f 100644
--- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
@@ -4,7 +4,7 @@ use base 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2237; }
+sub council_area_id { return 2237; }
sub council_area { return 'Oxfordshire'; }
sub council_name { return 'Oxfordshire County Council'; }
sub council_url { return 'oxfordshire'; }
@@ -96,7 +96,7 @@ sub problem_response_days {
return 10 if $p->category eq 'Utilities';
return 10 if $p->category eq 'Vegetation';
- return undef;
+ return 0;
}
sub reports_ordering {
@@ -106,12 +106,35 @@ sub reports_ordering {
sub pin_colour {
my ( $self, $p, $context ) = @_;
return 'grey' unless $self->owns_problem( $p );
- 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 'grey' if $p->is_closed;
+ return 'green' if $p->is_fixed;
+ return 'yellow' if $p->state eq 'confirmed';
+ return 'orange'; # all the other `open_states` like "in progress"
+}
+
+sub pin_new_report_colour {
return 'yellow';
}
+sub path_to_pin_icons {
+ return '/cobrands/oxfordshire/images/';
+}
+
+sub pin_hover_title {
+ my ($self, $problem, $title) = @_;
+ my $state = FixMyStreet::DB->resultset("State")->display($problem->state, 1);
+ return "$state: $title";
+}
+
+sub state_groups_inspect {
+ [
+ [ _('New'), [ 'confirmed', 'investigating' ] ],
+ [ _('Scheduled'), [ 'action scheduled' ] ],
+ [ _('Fixed'), [ 'fixed - council' ] ],
+ [ _('Closed'), [ 'not responsible', 'duplicate', 'unable to fix' ] ],
+ ]
+}
+
sub open311_config {
my ($self, $row, $h, $params) = @_;
diff --git a/perllib/FixMyStreet/Cobrand/Stevenage.pm b/perllib/FixMyStreet/Cobrand/Stevenage.pm
index 2c305d326..28734b14b 100644
--- a/perllib/FixMyStreet/Cobrand/Stevenage.pm
+++ b/perllib/FixMyStreet/Cobrand/Stevenage.pm
@@ -4,7 +4,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2347; }
+sub council_area_id { return 2347; }
sub council_area { return 'Stevenage'; }
sub council_name { return 'Stevenage Council'; }
sub council_url { return 'stevenage'; }
diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm
index 945af48f8..e1f5e565f 100644
--- a/perllib/FixMyStreet/Cobrand/UK.pm
+++ b/perllib/FixMyStreet/Cobrand/UK.pm
@@ -5,6 +5,7 @@ use strict;
use JSON::MaybeXS;
use mySociety::MaPit;
use mySociety::VotingArea;
+use Utils;
sub country { return 'GB'; }
sub area_types { [ 'DIS', 'LBO', 'MTD', 'UTA', 'CTY', 'COI', 'LGD' ] }
@@ -31,12 +32,11 @@ sub disambiguate_location {
sub process_open311_extras {
my $self = shift;
my $ctx = shift;
- my $body_id = shift;
+ my $body = shift;
my $extra = shift;
my $fields = shift || [];
- # XXX Hardcoded body ID matching mapit area ID
- if ( $body_id eq '2482' ) {
+ if ( $body && $body->name =~ /Bromley/ ) {
my @fields = ( 'fms_extra_title', @$fields );
for my $field ( @fields ) {
my $value = $ctx->get_param($field);
@@ -117,21 +117,27 @@ sub short_name {
}
sub find_closest {
- my ( $self, $latitude, $longitude, $problem ) = @_;
+ my ( $self, $problem, $as_data ) = @_;
- my $str = $self->SUPER::find_closest( $latitude, $longitude, $problem );
+ my $data = $self->SUPER::find_closest($problem, $as_data);
- my $url = "http://mapit.mysociety.org/nearest/4326/$longitude,$latitude";
+ my $mapit_url = FixMyStreet->config('MAPIT_URL');
+ my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude;
+ my $url = $mapit_url . "nearest/4326/$lon,$lat";
my $j = LWP::Simple::get($url);
if ($j) {
$j = JSON->new->utf8->allow_nonref->decode($j);
if ($j->{postcode}) {
- $str .= sprintf(_("Nearest postcode to the pin placed on the map (automatically generated): %s (%sm away)"),
- $j->{postcode}{postcode}, $j->{postcode}{distance}) . "\n\n";
+ if ($as_data) {
+ $data->{postcode} = $j->{postcode};
+ } else {
+ $data .= sprintf(_("Nearest postcode to the pin placed on the map (automatically generated): %s (%sm away)"),
+ $j->{postcode}{postcode}, $j->{postcode}{distance}) . "\n\n";
+ }
}
}
- return $str;
+ return $data;
}
sub reports_body_check {
@@ -248,25 +254,25 @@ sub council_rss_alert_options {
push @options, {
type => 'area',
id => sprintf( 'area:%s:%s', $district->{id}, $district->{id_name} ),
- text => $district_name,
+ text => sprintf( _('Problems within %s'), $district_name ),
rss_text => sprintf( _('RSS feed for %s'), $district_name ),
uri => $c->uri_for( '/rss/area/' . $district->{short_name} )
}, {
type => 'area',
id => sprintf( 'area:%s:%s:%s:%s', $district->{id}, $d_ward->{id}, $district->{id_name}, $d_ward->{id_name} ),
- text => sprintf( _('%s ward, %s'), $d_ward_name, $district_name ),
+ text => sprintf( _('Problems within %s ward, %s'), $d_ward_name, $district_name ),
rss_text => sprintf( _('RSS feed for %s ward, %s'), $d_ward_name, $district_name ),
uri => $c->uri_for( '/rss/area/' . $district->{short_name} . '/' . $d_ward->{short_name} )
}, {
type => 'area',
id => sprintf( 'area:%s:%s', $county->{id}, $county->{id_name} ),
- text => $county_name,
+ text => sprintf( _('Problems within %s'), $county_name ),
rss_text => sprintf( _('RSS feed for %s'), $county_name ),
uri => $c->uri_for( '/rss/area/' . $county->{short_name} )
}, {
type => 'area',
id => sprintf( 'area:%s:%s:%s:%s', $county->{id}, $c_ward->{id}, $county->{id_name}, $c_ward->{id_name} ),
- text => sprintf( _('%s ward, %s'), $c_ward_name, $county_name ),
+ text => sprintf( _('Problems within %s ward, %s'), $c_ward_name, $county_name ),
rss_text => sprintf( _('RSS feed for %s ward, %s'), $c_ward_name, $county_name ),
uri => $c->uri_for( '/rss/area/' . $county->{short_name} . '/' . $c_ward->{short_name} )
};
@@ -274,26 +280,26 @@ sub council_rss_alert_options {
push @reported_to_options, {
type => 'council',
id => sprintf( 'council:%s:%s', $district->{id}, $district->{id_name} ),
- text => $district->{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} ),
rss_text => sprintf( _('RSS feed of %s, within %s ward'), $district->{name}, $d_ward->{name}),
- text => sprintf( _('%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} ),
}, {
type => 'council',
id => sprintf( 'council:%s:%s', $county->{id}, $county->{id_name} ),
- text => $county->{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} ),
rss_text => sprintf( _('RSS feed of %s, within %s ward'), $county->{name}, $c_ward->{name}),
- text => sprintf( _('%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} ),
};
@@ -321,15 +327,11 @@ sub report_check_for_errors {
);
}
- # XXX Hardcoded body ID matching mapit area ID
if ( $report->bodies_str && $report->detail ) {
# Custom character limit:
- # Bromley Council
- if ( $report->bodies_str eq '2482' && length($report->detail) > 1750 ) {
+ 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 );
- }
- # Oxfordshire
- if ( $report->bodies_str eq '2237' && length($report->detail) > 1700 ) {
+ } 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 );
}
}
@@ -377,7 +379,7 @@ sub link_to_council_cobrand {
my $handler = $self->get_body_handler_for_problem($problem);
$self->{c}->log->debug( sprintf "bodies: %s areas: %s self: %s handler: %s", $problem->bodies_str, $problem->areas, $self->moniker, $handler->moniker );
my $bodies_str_ids = $problem->bodies_str_ids;
- if ( !mySociety::Config::get('AREA_LINKS_FROM_PROBLEMS') &&
+ if ( !FixMyStreet->config('AREA_LINKS_FROM_PROBLEMS') &&
scalar(@$bodies_str_ids) == 1 && $handler->is_council &&
$handler->moniker ne $self->{c}->cobrand->moniker
) {
diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
index e0b6b5298..b82e170b6 100644
--- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm
+++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
@@ -1,7 +1,5 @@
package FixMyStreet::Cobrand::UKCouncils;
-use base 'FixMyStreet::Cobrand::UK';
-
-# XXX Things using this cobrand base assume that a body ID === MapIt area ID
+use parent 'FixMyStreet::Cobrand::UK';
use strict;
use warnings;
@@ -40,16 +38,32 @@ sub restriction {
return { cobrand => shift->moniker };
}
+# UK cobrands assume that each MapIt area ID maps both ways with one body
+sub body {
+ my $self = shift;
+ my $body = FixMyStreet::DB->resultset('Body')->search({
+ 'body_areas.area_id' => $self->council_area_id
+ }, { join => 'body_areas' })->first;
+ return $body;
+}
+
sub problems_restriction {
my ($self, $rs) = @_;
return $rs if FixMyStreet->staging_flag('skip_checks');
- return $rs->to_body($self->council_id);
+ return $rs->to_body($self->body);
+}
+
+sub problems_on_map_restriction {
+ my ($self, $rs) = @_;
+ # If we're a two-tier council show all problems on the map and not just
+ # those for this cobrand's council to reduce duplicate reports.
+ return $self->is_two_tier ? $rs : $self->problems_restriction($rs);
}
sub updates_restriction {
my ($self, $rs) = @_;
return $rs if FixMyStreet->staging_flag('skip_checks');
- return $rs->to_body($self->council_id);
+ return $rs->to_body($self->body);
}
sub users_restriction {
@@ -75,7 +89,7 @@ sub users_restriction {
)->as_query;
my $or_query = [
- from_body => $self->council_id,
+ from_body => $self->body->id,
'me.id' => [ { -in => $problem_user_ids }, { -in => $update_user_ids } ],
];
if ($self->can('admin_user_domain')) {
@@ -108,7 +122,7 @@ sub area_check {
return 1 if FixMyStreet->staging_flag('skip_checks');
my $councils = $params->{all_areas};
- my $council_match = defined $councils->{$self->council_id};
+ my $council_match = defined $councils->{$self->council_area_id};
if ($council_match) {
return 1;
}
@@ -164,7 +178,7 @@ sub owns_problem {
@bodies = values %{$report->bodies};
}
my %areas = map { %{$_->areas} } @bodies;
- return $areas{$self->council_id} ? 1 : undef;
+ return $areas{$self->council_area_id} ? 1 : undef;
}
# If the council is two-tier then show pins for the other council as grey
@@ -192,7 +206,7 @@ sub admin_allow_user {
my ( $self, $user ) = @_;
return 1 if $user->is_superuser;
return undef unless defined $user->from_body;
- return $user->from_body->id == $self->council_id;
+ return $user->from_body->areas->{$self->council_area_id};
}
sub available_permissions {
diff --git a/perllib/FixMyStreet/Cobrand/Warwickshire.pm b/perllib/FixMyStreet/Cobrand/Warwickshire.pm
index e52188311..5fa967c62 100644
--- a/perllib/FixMyStreet/Cobrand/Warwickshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Warwickshire.pm
@@ -4,7 +4,7 @@ use base 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
-sub council_id { return 2243; }
+sub council_area_id { return 2243; }
sub council_area { return 'Warwickshire'; }
sub council_name { return 'Warwickshire County Council'; }
sub council_url { return 'warwickshire'; }
diff --git a/perllib/FixMyStreet/Cobrand/WestBerkshire.pm b/perllib/FixMyStreet/Cobrand/WestBerkshire.pm
index 7e98187bb..e13d701a6 100644
--- a/perllib/FixMyStreet/Cobrand/WestBerkshire.pm
+++ b/perllib/FixMyStreet/Cobrand/WestBerkshire.pm
@@ -4,7 +4,7 @@ use base 'FixMyStreet::Cobrand::UK';
use strict;
use warnings;
-sub council_id { 2619 }
+sub council_area_id { 2619 }
# non standard west berks end points
sub open311_pre_send {
diff --git a/perllib/FixMyStreet/Cobrand/Whitelabel.pm b/perllib/FixMyStreet/Cobrand/Whitelabel.pm
new file mode 100644
index 000000000..42a23e40f
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Whitelabel.pm
@@ -0,0 +1,13 @@
+package FixMyStreet::Cobrand::Whitelabel;
+use base 'FixMyStreet::Cobrand::UKCouncils';
+
+sub path_to_web_templates {
+ my $self = shift;
+ return [
+ FixMyStreet->path_to( 'templates/web', $self->moniker ),
+ FixMyStreet->path_to( 'templates/web/whitelabel' ),
+ FixMyStreet->path_to( 'templates/web/fixmystreet-uk-councils' ),
+ ];
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index dca722224..de4a5262a 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -54,6 +54,11 @@ you already have, and the countres set so that they shouldn't in future.
=cut
+sub setup_states {
+ FixMyStreet::DB::Result::Problem->visible_states_add('unconfirmed');
+ FixMyStreet::DB::Result::Problem->visible_states_remove('investigating');
+}
+
sub shorten_recency_if_new_greater_than_fixed {
return 0;
}
@@ -67,7 +72,7 @@ sub pin_colour {
# This isn't used
sub find_closest {
- my ( $self, $latitude, $longitude, $problem ) = @_;
+ my ( $self, $problem ) = @_;
return '';
}
@@ -519,7 +524,7 @@ sub admin_report_edit {
# Can change category to any other
my @categories = $c->model('DB::Contact')->not_deleted->all;
- $c->stash->{categories} = [ map { $_->category } @categories ];
+ $c->stash->{category_options} = [ map { { name => $_->category, value => $_->category } } @categories ];
} elsif ($type eq 'dm') {
@@ -534,7 +539,7 @@ sub admin_report_edit {
# Can change category to any other
my @categories = $c->model('DB::Contact')->not_deleted->all;
- $c->stash->{categories} = [ map { $_->category } @categories ];
+ $c->stash->{category_options} = [ map { { name => $_->category, value => $_->category } } @categories ];
}
diff --git a/perllib/FixMyStreet/DB.pm b/perllib/FixMyStreet/DB.pm
index d920c809f..cee66b434 100644
--- a/perllib/FixMyStreet/DB.pm
+++ b/perllib/FixMyStreet/DB.pm
@@ -1,22 +1,13 @@
-use utf8;
package FixMyStreet::DB;
-# Created by DBIx::Class::Schema::Loader
-# DO NOT MODIFY THE FIRST PART OF THIS FILE
-
use strict;
use warnings;
+use FixMyStreet::DB::Schema;
-use base 'DBIx::Class::Schema';
-
-__PACKAGE__->load_namespaces;
-
-
-# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CjFpUvon7KggFM7OF7VK/w
+my $schema;
-use FixMyStreet;
+sub schema { $schema ||= FixMyStreet::DB::Schema->clone }
-__PACKAGE__->connection(FixMyStreet->dbic_connect_info);
+sub resultset { shift->schema->resultset(@_) }
1;
diff --git a/perllib/FixMyStreet/DB/Factories.pm b/perllib/FixMyStreet/DB/Factories.pm
new file mode 100644
index 000000000..ec4dd630a
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Factories.pm
@@ -0,0 +1,173 @@
+use FixMyStreet::DB;
+
+package FixMyStreet::DB::Factory::Base;
+
+use parent "DBIx::Class::Factory";
+
+sub find_or_create {
+ my ($class, $fields) = @_;
+ my $key_field = $class->key_field;
+ my $id = $class->get_fields($fields)->{$key_field};
+ my $rs = $class->_class_data->{resultset};
+ my $obj = $rs->find({ $key_field => $id });
+ return $obj if $obj;
+ return $class->create($fields);
+}
+
+#######################
+
+package FixMyStreet::DB::Factory::Problem;
+
+use parent "DBIx::Class::Factory";
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("Problem"));
+
+__PACKAGE__->exclude(['body']);
+
+__PACKAGE__->fields({
+ postcode => '',
+ title => __PACKAGE__->seq(sub { 'Title #' . (shift()+1) }),
+ detail => __PACKAGE__->seq(sub { 'Detail #' . (shift()+1) }),
+ name => __PACKAGE__->callback(sub { shift->get('user')->name }),
+ bodies_str => __PACKAGE__->callback(sub { shift->get('body')->id }),
+ confirmed => \'current_timestamp',
+ whensent => \'current_timestamp',
+ state => 'confirmed',
+ cobrand => 'default',
+ latitude => 0,
+ longitude => 0,
+ areas => '',
+ used_map => 't',
+ anonymous => 'f',
+ category => 'Other',
+});
+
+#######################
+
+package FixMyStreet::DB::Factory::Body;
+
+use parent -norequire, "FixMyStreet::DB::Factory::Base";
+use mySociety::MaPit;
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("Body"));
+
+__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);
+ $area->{name};
+ }),
+ body_areas => __PACKAGE__->callback(sub {
+ my $area_id = shift->get('area_id');
+ [ { area_id => $area_id } ]
+ }),
+ contacts => __PACKAGE__->callback(sub {
+ my $categories = shift->get('categories');
+ push @$categories, 'Other' unless @$categories;
+ [ map { FixMyStreet::DB::Factory::Contact->get_fields({ category => $_ }) } @$categories ];
+ }),
+});
+
+sub key_field { 'id' }
+
+#######################
+
+package FixMyStreet::DB::Factory::Contact;
+
+use parent "DBIx::Class::Factory";
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("Contact"));
+
+__PACKAGE__->fields({
+ body_id => __PACKAGE__->callback(sub {
+ my $fields = shift;
+ return $fields->get('body')->id if $fields->get('body');
+ }),
+ category => 'Other',
+ email => __PACKAGE__->callback(sub {
+ my $category = shift->get('category');
+ (my $email = lc $_) =~ s/ /-/g;
+ lc $category . '@example.org';
+ }),
+ state => 'confirmed',
+ editor => 'Factory',
+ whenedited => \'current_timestamp',
+ note => 'Created by factory',
+});
+
+#######################
+
+package FixMyStreet::DB::Factory::ResponseTemplate;
+
+use parent -norequire, "FixMyStreet::DB::Factory::Base";
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("ResponseTemplate"));
+
+__PACKAGE__->fields({
+ text => __PACKAGE__->seq(sub { 'Template text #' . (shift()+1) }),
+});
+
+#######################
+
+package FixMyStreet::DB::Factory::ResponsePriority;
+
+use parent "DBIx::Class::Factory";
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("ResponsePriority"));
+
+__PACKAGE__->fields({
+ name => __PACKAGE__->seq(sub { 'Priority #' . (shift()+1) }),
+ description => __PACKAGE__->seq(sub { 'Description #' . (shift()+1) }),
+});
+
+#######################
+
+package FixMyStreet::DB::Factory::Comment;
+
+use parent "DBIx::Class::Factory";
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("Comment"));
+
+__PACKAGE__->fields({
+ anonymous => 'f',
+ name => __PACKAGE__->callback(sub { shift->get('user')->name }),
+ text => __PACKAGE__->seq(sub { 'Comment #' . (shift()+1) }),
+ confirmed => \'current_timestamp',
+ state => 'confirmed',
+ cobrand => 'default',
+ mark_fixed => 0,
+});
+
+#######################
+
+package FixMyStreet::DB::Factory::User;
+
+use parent -norequire, "FixMyStreet::DB::Factory::Base";
+
+__PACKAGE__->resultset(FixMyStreet::DB->resultset("User"));
+
+__PACKAGE__->exclude(['body', 'permissions']);
+
+__PACKAGE__->fields({
+ name => 'User',
+ email => 'user@example.org',
+ password => 'password',
+ from_body => __PACKAGE__->callback(sub {
+ my $fields = shift;
+ if (my $body = $fields->get('body')) {
+ return $body->id;
+ }
+ }),
+ user_body_permissions => __PACKAGE__->callback(sub {
+ my $fields = shift;
+ my $body = $fields->get('body');
+ my $permissions = $fields->get('permissions');
+ [ map { { body_id => $body->id, permission_type => $_ } } @$permissions ];
+ }),
+});
+
+sub key_field { 'email' }
+
+1;
diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm
index 82015ad2d..6481d5cfc 100644
--- a/perllib/FixMyStreet/DB/Result/Body.pm
+++ b/perllib/FixMyStreet/DB/Result/Body.pm
@@ -121,18 +121,52 @@ __PACKAGE__->has_many(
# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BOJANVwg3kR/1VjDq0LykA
+use Moo;
+use namespace::clean;
+
+with 'FixMyStreet::Roles::Translatable';
+
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 || {} );
}
+__PACKAGE__->might_have(
+ "translations",
+ "FixMyStreet::DB::Result::Translation",
+ sub {
+ my $args = shift;
+ return {
+ "$args->{foreign_alias}.object_id" => { -ident => "$args->{self_alias}.id" },
+ "$args->{foreign_alias}.tbl" => { '=' => \"?" },
+ "$args->{foreign_alias}.col" => { '=' => \"?" },
+ "$args->{foreign_alias}.lang" => { '=' => \"?" },
+ };
+ },
+ { cascade_copy => 0, cascade_delete => 0 },
+);
+
+around name => \&translate_around;
+
sub areas {
my $self = shift;
my %ids = map { $_->area_id => 1 } $self->body_areas->all;
return \%ids;
}
+sub first_area_children {
+ my ( $self, $c ) = @_;
+
+ my $area_id = $self->body_areas->first->area_id;
+
+ my $children = mySociety::MaPit::call('area/children', $area_id,
+ type => $c->cobrand->area_types_children,
+ );
+
+ return $children;
+}
+
=head2 get_cobrand_handler
Get a cobrand object for this body, if there is one.
diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm
index cf1ba444d..562f29693 100644
--- a/perllib/FixMyStreet/DB/Result/Comment.pm
+++ b/perllib/FixMyStreet/DB/Result/Comment.pm
@@ -101,6 +101,7 @@ use Moo;
use namespace::clean -except => [ 'meta' ];
with 'FixMyStreet::Roles::Abuser',
+ 'FixMyStreet::Roles::Extra',
'FixMyStreet::Roles::PhotoSet';
my $stz = sub {
@@ -128,9 +129,10 @@ sub check_for_errors {
unless $self->text =~ m/\S/;
# Bromley Council custom character limit
- if ( $self->text && $self->problem && $self->problem->bodies_str
- && $self->problem->bodies_str eq '2482' && length($self->text) > 1750 ) {
- $errors{update} = sprintf( _('Updates are limited to %s characters in length. Please shorten your update'), 1750 );
+ if ( $self->text && $self->problem && $self->problem->bodies_str) {
+ if ($self->problem->to_body_named('Bromley') && length($self->text) > 1750) {
+ $errors{update} = sprintf( _('Updates are limited to %s characters in length. Please shorten your update'), 1750 );
+ }
}
return \%errors;
@@ -149,6 +151,11 @@ sub confirm {
$self->confirmed( \'current_timestamp' );
}
+sub url {
+ my $self = shift;
+ return "/report/" . $self->problem_id . '#update_' . $self->id;
+}
+
sub photos {
my $self = shift;
my $photoset = $self->get_photoset;
@@ -169,22 +176,6 @@ sub photos {
return \@photos;
}
-=head2 problem_state_display
-
-Returns a string suitable for display lookup in the update meta section.
-Removes the '- council/user' bit from fixed states.
-
-=cut
-
-sub problem_state_display {
- my $self = shift;
-
- my $state = $self->problem_state;
- $state =~ s/ -.*$//;
-
- return $state;
-}
-
=head2 latest_moderation_log_entry
Return most recent ModerationLog object
@@ -236,8 +227,6 @@ sub meta_line {
my $meta = '';
- $c->stash->{last_state} ||= '';
-
if ($self->anonymous or !$self->name) {
$meta = sprintf( _( 'Posted anonymously at %s' ), Utils::prettify_dt( $self->confirmed ) )
} elsif ($self->user->from_body) {
@@ -248,68 +237,54 @@ sub meta_line {
} elsif ($body eq 'Royal Borough of Greenwich') {
$body = "$body <img src='/cobrands/greenwich/favicon.png' alt=''>";
}
- if ($c->user_exists and $c->user->has_permission_to('view_body_contribute_details', $self->problem->bodies_str_ids)) {
- $meta = sprintf( _( 'Posted by <strong>%s</strong> (%s) at %s' ), $body, $user_name, Utils::prettify_dt( $self->confirmed ) );
+ my $can_view_contribute = $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 ) );
+ } else {
+ $meta = sprintf( _( 'Posted by <strong>%s</strong> at %s' ), $body, Utils::prettify_dt( $self->confirmed ) );
+ }
} else {
- $meta = sprintf( _( 'Posted by <strong>%s</strong> at %s' ), $body, Utils::prettify_dt( $self->confirmed ) );
+ if ($can_view_contribute) {
+ $meta = sprintf( _( 'Updated by <strong>%s</strong> (%s) at %s' ), $body, $user_name, Utils::prettify_dt( $self->confirmed ) );
+ } else {
+ $meta = sprintf( _( 'Updated by <strong>%s</strong> at %s' ), $body, Utils::prettify_dt( $self->confirmed ) );
+ }
}
} else {
$meta = sprintf( _( 'Posted by %s at %s' ), FixMyStreet::Template::html_filter($self->name), Utils::prettify_dt( $self->confirmed ) )
}
+ if ($self->get_extra_metadata('defect_raised')) {
+ $meta .= ', ' . _( 'and a defect raised' );
+ }
+
+ return $meta;
+};
+
+sub problem_state_display {
+ my ( $self, $c ) = @_;
+
my $update_state = '';
+ my $cobrand = $c->cobrand->moniker;
if ($self->mark_fixed) {
- $update_state = _( 'marked as fixed' );
+ return FixMyStreet::DB->resultset("State")->display('fixed', 1);
} elsif ($self->mark_open) {
- $update_state = _( 'reopened' );
+ return FixMyStreet::DB->resultset("State")->display('confirmed', 1);
} elsif ($self->problem_state) {
- my $state = $self->problem_state_display;
-
- if ($state eq 'confirmed') {
- if ($c->stash->{last_state}) {
- $update_state = _( 'reopened' )
- }
- } elsif ($state eq 'investigating') {
- $update_state = _( 'marked as investigating' )
- } elsif ($state eq 'planned') {
- $update_state = _( 'marked as planned' )
- } elsif ($state eq 'in progress') {
- $update_state = _( 'marked as in progress' )
- } elsif ($state eq 'action scheduled') {
- $update_state = _( 'marked as action scheduled' )
- } elsif ($state eq 'closed') {
- $update_state = _( 'marked as closed' )
- } elsif ($state eq 'fixed') {
- $update_state = _( 'marked as fixed' )
- } elsif ($state eq 'unable to fix') {
- $update_state = _( 'marked as no further action' )
- } elsif ($state eq 'not responsible') {
- $update_state = _( "marked as not the council's responsibility" )
- } elsif ($state eq 'duplicate') {
- $update_state = _( 'closed as a duplicate report' )
- } elsif ($state eq 'internal referral') {
- $update_state = _( 'marked as an internal referral' )
- }
-
- if ($c->cobrand->moniker eq 'bromley' || (
- $self->problem->bodies_str &&
- $self->problem->bodies_str eq '2482'
- )) {
- if ($state eq 'not responsible') {
- $update_state = 'marked as third party responsibility'
+ my $state = $self->problem_state;
+ if ($state eq 'not responsible') {
+ $update_state = _( "not the council's responsibility" );
+ if ($cobrand eq 'bromley' || $self->problem->to_body_named('Bromley')) {
+ $update_state = 'third party responsibility';
}
+ } else {
+ $update_state = FixMyStreet::DB->resultset("State")->display($state, 1);
}
-
- }
-
- if ($update_state ne $c->stash->{last_state} and $update_state) {
- $meta .= ", $update_state";
}
- $c->stash->{last_state} = $update_state;
-
- return $meta;
-};
+ return $update_state;
+}
1;
diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm
index a620b7358..f9cbf1c44 100644
--- a/perllib/FixMyStreet/DB/Result/Contact.pm
+++ b/perllib/FixMyStreet/DB/Result/Contact.pm
@@ -11,8 +11,17 @@ use base 'DBIx::Class::Core';
__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
__PACKAGE__->table("contacts");
__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "contacts_id_seq",
+ },
"body_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+ "category",
+ { data_type => "text", default_value => "Other", is_nullable => 0 },
"email",
{ data_type => "text", is_nullable => 0 },
"editor",
@@ -21,19 +30,6 @@ __PACKAGE__->add_columns(
{ data_type => "timestamp", is_nullable => 0 },
"note",
{ data_type => "text", is_nullable => 0 },
- "confirmed",
- { data_type => "boolean", is_nullable => 0 },
- "category",
- { data_type => "text", default_value => "Other", is_nullable => 0 },
- "deleted",
- { data_type => "boolean", is_nullable => 0 },
- "id",
- {
- data_type => "integer",
- is_auto_increment => 1,
- is_nullable => 0,
- sequence => "contacts_id_seq",
- },
"extra",
{ data_type => "text", is_nullable => 1 },
"non_public",
@@ -46,6 +42,8 @@ __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"]);
@@ -75,8 +73,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:f9VepR/oPyr3z6PUpJ4w2A
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-08 20:45:04
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t/VtPP11R8bbqPZdEVXffw
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
@@ -84,12 +82,18 @@ __PACKAGE__->rabx_column('extra');
use Moo;
use namespace::clean -except => [ 'meta' ];
-with 'FixMyStreet::Roles::Extra';
+with 'FixMyStreet::Roles::Extra',
+ 'FixMyStreet::Roles::Translatable';
__PACKAGE__->many_to_many( response_templates => 'contact_response_templates', 'response_template' );
__PACKAGE__->many_to_many( response_priorities => 'contact_response_priorities', 'response_priority' );
__PACKAGE__->many_to_many( defect_types => 'contact_defect_types', 'defect_type' );
+sub category_display {
+ my $self = shift;
+ $self->translate_column('category');
+}
+
sub get_metadata_for_input {
my $self = shift;
my $id_field = $self->id_field;
diff --git a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm
index 7126d91c9..c90bb9d66 100644
--- a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm
+++ b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm
@@ -26,22 +26,20 @@ __PACKAGE__->add_columns(
{ data_type => "text", default_value => "Other", is_nullable => 0 },
"email",
{ data_type => "text", is_nullable => 0 },
- "confirmed",
- { data_type => "boolean", is_nullable => 0 },
- "deleted",
- { data_type => "boolean", 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.07017 @ 2012-12-12 16:37:16
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sxflEBBn0Mn0s3MroWnWFA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-08 20:45:04
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HTt0g29yXTM/WyHKN179FA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index 84db41490..3b622b561 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -206,6 +206,7 @@ my $IM = eval {
with 'FixMyStreet::Roles::Abuser',
'FixMyStreet::Roles::Extra',
+ 'FixMyStreet::Roles::Translatable',
'FixMyStreet::Roles::PhotoSet';
=head2
@@ -219,15 +220,8 @@ HASHREF.
=cut
sub open_states {
- my $states = {
- 'confirmed' => 1,
- 'investigating' => 1,
- 'in progress' => 1,
- 'planned' => 1,
- 'action scheduled' => 1,
- };
-
- return wantarray ? keys %{$states} : $states;
+ my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->open};
+ return wantarray ? @states : { map { $_ => 1 } @states };
}
=head2
@@ -241,13 +235,9 @@ HASHREF.
=cut
sub fixed_states {
- my $states = {
- 'fixed' => 1,
- 'fixed - user' => 1,
- 'fixed - council' => 1,
- };
-
- return wantarray ? keys %{ $states } : $states;
+ my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->fixed};
+ push @states, 'fixed - user', 'fixed - council' if @states;
+ return wantarray ? @states : { map { $_ => 1 } @states };
}
=head2
@@ -261,18 +251,10 @@ HASHREF.
=cut
sub closed_states {
- my $states = {
- 'closed' => 1,
- 'unable to fix' => 1,
- 'not responsible' => 1,
- 'duplicate' => 1,
- 'internal referral' => 1,
- };
-
- return wantarray ? keys %{$states} : $states;
+ my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->closed};
+ return wantarray ? @states : { map { $_ => 1 } @states };
}
-
=head2
@states = FixMyStreet::DB::Problem::all_states();
@@ -288,21 +270,10 @@ sub all_states {
'hidden' => 1,
'partial' => 1,
'unconfirmed' => 1,
- 'confirmed' => 1,
- 'investigating' => 1,
- 'in progress' => 1,
- 'planned' => 1,
- 'action scheduled' => 1,
- 'fixed' => 1,
'fixed - council' => 1,
'fixed - user' => 1,
- 'unable to fix' => 1,
- 'not responsible' => 1,
- 'duplicate' => 1,
- 'closed' => 1,
- 'internal referral' => 1,
};
-
+ map { $states->{$_->label} = 1 } @{FixMyStreet::DB->resultset("State")->states};
return wantarray ? keys %{$states} : $states;
}
@@ -322,75 +293,31 @@ my $hidden_states = {
'unconfirmed' => 1,
};
-my $visible_states = {
- map {
- $hidden_states->{$_} ? () : ($_ => 1)
- } all_states()
-};
- ## e.g.:
- # 'confirmed' => 1,
- # 'investigating' => 1,
- # 'in progress' => 1,
- # 'planned' => 1,
- # 'action scheduled' => 1,
- # 'fixed' => 1,
- # 'fixed - council' => 1,
- # 'fixed - user' => 1,
- # 'unable to fix' => 1,
- # 'not responsible' => 1,
- # 'duplicate' => 1,
- # 'closed' => 1,
- # 'internal referral' => 1,
-
sub hidden_states {
return wantarray ? keys %{$hidden_states} : $hidden_states;
}
sub visible_states {
- return wantarray ? keys %{$visible_states} : $visible_states;
+ my %visible_states = map {
+ $hidden_states->{$_} ? () : ($_ => 1)
+ } all_states();
+ return wantarray ? keys %visible_states : \%visible_states;
}
sub visible_states_add {
my ($self, @states) = @_;
for my $state (@states) {
delete $hidden_states->{$state};
- $visible_states->{$state} = 1;
}
}
sub visible_states_remove {
my ($self, @states) = @_;
for my $state (@states) {
- delete $visible_states->{$state};
$hidden_states->{$state} = 1;
}
}
-=head2
-
- @states = FixMyStreet::DB::Problem::council_states();
-
-Get a list of states that are availble to council users. If called in
-array context then returns an array of names, otherwise returns a
-HASHREF.
-
-=cut
-sub council_states {
- my $states = {
- 'confirmed' => 1,
- 'investigating' => 1,
- 'action scheduled' => 1,
- 'in progress' => 1,
- 'fixed - council' => 1,
- 'unable to fix' => 1,
- 'not responsible' => 1,
- 'duplicate' => 1,
- 'internal referral' => 1,
- };
-
- return wantarray ? keys %{$states} : $states;
-}
-
my $stz = sub {
my ( $orig, $self ) = ( shift, shift );
my $s = $self->$orig(@_);
@@ -456,12 +383,6 @@ sub check_for_errors {
$errors{category} = _('Please choose a category');
$self->category(undef);
}
- elsif ($self->category
- && $self->category eq _('-- Pick a property type --') )
- {
- $errors{category} = _('Please choose a property type');
- $self->category(undef);
- }
return \%errors;
}
@@ -489,6 +410,11 @@ sub confirm {
return 1;
}
+sub category_display {
+ my $self = shift;
+ $self->translate_column('category');
+}
+
sub bodies_str_ids {
my $self = shift;
return [] unless $self->bodies_str;
@@ -502,12 +428,36 @@ Returns a hashref of bodies to which a report was sent.
=cut
-sub bodies($) {
+has bodies => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ return {} unless $self->bodies_str;
+ my $cache = $self->result_source->schema->cache;
+ return $cache->{bodies}{$self->bodies_str} if $cache->{bodies}{$self->bodies_str};
+
+ my $bodies = $self->bodies_str_ids;
+ my @bodies = $self->result_source->schema->resultset('Body')->search(
+ { id => $bodies },
+ { prefetch => 'body_areas' },
+ )->all;
+ $cache->{bodies}{$self->bodies_str} = { map { $_->id => $_ } @bodies };
+ return $cache->{bodies}{$self->bodies_str};
+ },
+);
+
+sub body_names($) {
my $self = shift;
- return {} unless $self->bodies_str;
- my $bodies = $self->bodies_str_ids;
- my @bodies = $self->result_source->schema->resultset('Body')->search({ id => $bodies })->all;
- return { map { $_->id => $_ } @bodies };
+ my $bodies = $self->bodies;
+ my @names = map { $_->name } values %$bodies;
+ return \@names;
+}
+
+sub to_body_named($$) {
+ my ($self, $re) = @_;
+ my $names = join(',,', @{$self->body_names});
+ $names =~ /$re/;
}
=head2 url
@@ -609,19 +559,6 @@ sub is_visible {
return exists $self->visible_states->{ $self->state } ? 1 : 0;
}
-=head2 state_display
-
-Returns a string suitable for display lookup in the update meta section.
-Removes the '- council/user' bit from fixed states.
-
-=cut
-
-sub state_display {
- my $self = shift;
- (my $state = $self->state) =~ s/ -.*$//;
- return $state;
-}
-
=head2 meta_line
Returns a string to be used on a problem report page, describing some of the
@@ -635,7 +572,7 @@ sub meta_line {
my $date_time = Utils::prettify_dt( $problem->confirmed );
my $meta = '';
- my $category = $problem->category;
+ my $category = $problem->category_display;
$category = $c->cobrand->call_hook(change_category_text => $category) || $category;
if ( $problem->anonymous ) {
@@ -787,7 +724,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->bodies_str =~ /(2237|2550)/) {
+ if ($self->external_id && $self->send_method_used && $self->to_body_named('Oxfordshire|Angus')) {
return 1;
}
return 0;
@@ -924,6 +861,7 @@ sub as_hashref {
latitude => $self->latitude,
longitude => $self->longitude,
postcode => $self->postcode,
+ areas => $self->areas,
state => $self->state,
state_t => _( $self->state ),
used_map => $self->used_map,
@@ -953,15 +891,21 @@ sub photos {
my $id = $self->id;
my @photos = map {
my $cachebust = substr($_, 0, 8);
+ # Some Varnish configurations (e.g. on mySociety infra) strip cookies from
+ # images, which means image requests will be redirected to the login page
+ # if LOGIN_REQUIRED is set. To stop this happening, Varnish should be
+ # configured to not strip cookies if the cookie_passthrough param is
+ # present, which this line ensures will be if LOGIN_REQUIRED is set.
+ my $extra = (FixMyStreet->config('LOGIN_REQUIRED')) ? "&cookie_passthrough=1" : "";
my ($hash, $format) = split /\./, $_;
{
id => $hash,
- url_temp => "/photo/temp.$hash.$format",
- url_temp_full => "/photo/fulltemp.$hash.$format",
- url => "/photo/$id.$i.$format?$cachebust",
- url_full => "/photo/$id.$i.full.$format?$cachebust",
- url_tn => "/photo/$id.$i.tn.$format?$cachebust",
- url_fp => "/photo/$id.$i.fp.$format?$cachebust",
+ url_temp => "/photo/temp.$hash.$format$extra",
+ url_temp_full => "/photo/fulltemp.$hash.$format$extra",
+ url => "/photo/$id.$i.$format?$cachebust$extra",
+ url_full => "/photo/$id.$i.full.$format?$cachebust$extra",
+ url_tn => "/photo/$id.$i.tn.$format?$cachebust$extra",
+ url_fp => "/photo/$id.$i.fp.$format?$cachebust$extra",
idx => $i++,
}
} $photoset->all_ids;
@@ -1013,13 +957,14 @@ has get_cobrand_logged => (
sub pin_data {
my ($self, $c, $page, %opts) = @_;
my $colour = $c->cobrand->pin_colour($self, $page);
-
+ my $title = $opts{private} ? $self->title : $self->title_safe;
+ $title = $c->cobrand->call_hook(pin_hover_title => $self, $title) || $title;
{
latitude => $self->latitude,
longitude => $self->longitude,
colour => $colour,
id => $self->id,
- title => $opts{private} ? $self->title : $self->title_safe,
+ title => $title,
problem => $self,
type => $opts{type},
}
@@ -1082,6 +1027,7 @@ sub static_map {
if ($pin) {
my $im = Image::Magick->new;
$im->read(FixMyStreet->path_to('web', 'i', 'pin-yellow.png'));
+ $im->Scale( geometry => '48x64' );
$image->Composite(image => $im, gravity => 'NorthWest',
x => $pin->{px} - 24, y => $pin->{py} - 64);
}
@@ -1113,6 +1059,16 @@ has shortlisted_user => (
},
);
+sub set_duplicate_of {
+ my ($self, $other_id) = @_;
+ $self->set_extra_metadata( duplicate_of => $other_id );
+ my $dupe = $self->result_source->schema->resultset("Problem")->find($other_id);
+ my $dupes_duplicates = $dupe->get_extra_metadata('duplicates') || [];
+ push @$dupes_duplicates, $self->id;
+ $dupe->set_extra_metadata( duplicates => $dupes_duplicates );
+ $dupe->update;
+}
+
has duplicate_of => (
is => 'ro',
lazy => 1,
@@ -1130,8 +1086,9 @@ has duplicates => (
lazy => 1,
default => sub {
my $self = shift;
- my $rabx_id = RABX::serialise( $self->id );
- my @duplicates = $self->result_source->schema->resultset('Problem')->search({ extra => { like => "\%duplicate_of,$rabx_id%" } })->all;
+ my $duplicates = $self->get_extra_metadata("duplicates") || [];
+ return [] unless $duplicates && @$duplicates;
+ my @duplicates = $self->result_source->schema->resultset('Problem')->search({ id => $duplicates })->all;
return \@duplicates;
},
);
diff --git a/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm b/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm
new file mode 100644
index 000000000..27a6bd2c6
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm
@@ -0,0 +1,45 @@
+use utf8;
+package FixMyStreet::DB::Result::ReportExtraFields;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->table("report_extra_fields");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "report_extra_fields_id_seq",
+ },
+ "name",
+ { data_type => "text", is_nullable => 0 },
+ "cobrand",
+ { data_type => "text", is_nullable => 1 },
+ "language",
+ { data_type => "text", is_nullable => 1 },
+ "extra",
+ { data_type => "text", is_nullable => 1 },
+);
+__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
+
+__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
+__PACKAGE__->rabx_column('extra');
+
+use Moo;
+use namespace::clean -except => [ 'meta' ];
+
+with 'FixMyStreet::Roles::Extra';
+
+# 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/ResponsePriority.pm b/perllib/FixMyStreet/DB/Result/ResponsePriority.pm
index 44635d174..df54cfa08 100644
--- a/perllib/FixMyStreet/DB/Result/ResponsePriority.pm
+++ b/perllib/FixMyStreet/DB/Result/ResponsePriority.pm
@@ -28,6 +28,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 1 },
"external_id",
{ data_type => "text", is_nullable => 1 },
+ "is_default",
+ { data_type => "boolean", default_value => \"false", is_nullable => 0 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint("response_priorities_body_id_name_key", ["body_id", "name"]);
@@ -51,8 +53,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-12-14 17:12:09
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:glsO0fLK6fNvg4TmW1DMPg
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-09-12 09:32:53
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JBIHFnaLvXCAUjgwTSB3CQ
__PACKAGE__->many_to_many( contacts => 'contact_response_priorities', 'contact' );
diff --git a/perllib/FixMyStreet/DB/Result/State.pm b/perllib/FixMyStreet/DB/Result/State.pm
new file mode 100644
index 000000000..b8a35d42b
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/State.pm
@@ -0,0 +1,48 @@
+use utf8;
+package FixMyStreet::DB::Result::State;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->table("state");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "state_id_seq",
+ },
+ "label",
+ { data_type => "text", is_nullable => 0 },
+ "type",
+ { data_type => "text", is_nullable => 0 },
+ "name",
+ { data_type => "text", is_nullable => 0 },
+);
+__PACKAGE__->set_primary_key("id");
+__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
+
+use Moo;
+use namespace::clean;
+
+with 'FixMyStreet::Roles::Translatable';
+
+sub msgstr {
+ my $self = shift;
+ my $lang = $self->result_source->schema->lang;
+ return $self->name unless $lang && $self->translated->{name}{$lang};
+ return $self->translated->{name}{$lang}{msgstr};
+}
+
+1;
diff --git a/perllib/FixMyStreet/DB/Result/Translation.pm b/perllib/FixMyStreet/DB/Result/Translation.pm
new file mode 100644
index 000000000..fafc7ccf1
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/Translation.pm
@@ -0,0 +1,44 @@
+use utf8;
+package FixMyStreet::DB::Result::Translation;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->table("translation");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "translation_id_seq",
+ },
+ "tbl",
+ { data_type => "text", is_nullable => 0 },
+ "object_id",
+ { data_type => "integer", is_nullable => 0 },
+ "col",
+ { data_type => "text", is_nullable => 0 },
+ "lang",
+ { data_type => "text", is_nullable => 0 },
+ "msgstr",
+ { data_type => "text", is_nullable => 0 },
+);
+__PACKAGE__->set_primary_key("id");
+__PACKAGE__->add_unique_constraint(
+ "translation_tbl_object_id_col_lang_key",
+ ["tbl", "object_id", "col", "lang"],
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-14 23:24:32
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:///VNqg4BOuO29xKhnY8vw
+
+
+# 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/User.pm b/perllib/FixMyStreet/DB/Result/User.pm
index cf6de9a76..19adf5d49 100644
--- a/perllib/FixMyStreet/DB/Result/User.pm
+++ b/perllib/FixMyStreet/DB/Result/User.pm
@@ -204,6 +204,27 @@ sub alert_for_problem {
} );
}
+=head2 create_alert
+
+Sign a user up to receive alerts on a given problem
+
+=cut
+
+sub create_alert {
+ my ( $self, $id, $options ) = @_;
+ my $alert = $self->alert_for_problem($id);
+
+ unless ( $alert ) {
+ $alert = $self->alerts->create({
+ %$options,
+ alert_type => 'new_updates',
+ parameter => $id,
+ });
+ }
+
+ $alert->confirm();
+}
+
sub body {
my $self = shift;
return '' unless $self->from_body;
@@ -274,6 +295,16 @@ sub permissions {
sub has_permission_to {
my ($self, $permission_type, $body_ids) = @_;
+ # Nobody, including superusers, can have a permission which isn't available
+ # in the current cobrand.
+ my $cobrand = $self->result_source->schema->cobrand;
+ my $cobrand_perms = $cobrand->available_permissions;
+ my %available = map { %$_ } values %$cobrand_perms;
+ # The 'trusted' permission is never set in the cobrand's
+ # available_permissions (see note there in Default.pm) so include it here.
+ $available{trusted} = 1;
+ return 0 unless $available{$permission_type};
+
return 1 if $self->is_superuser;
return 0 if !$body_ids || (ref $body_ids && !@$body_ids);
$body_ids = [ $body_ids ] unless ref $body_ids;
@@ -391,9 +422,19 @@ sub active_planned_reports {
$self->planned_reports->search({ removed => undef });
}
+has active_user_planned_reports => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ [ $self->user_planned_reports->search({ removed => undef })->all ];
+ },
+);
+
sub is_planned_report {
my ($self, $problem) = @_;
- return $self->active_planned_reports->find({ id => $problem->id });
+ my $id = $problem->id;
+ return scalar grep { $_->report_id == $id } @{$self->active_user_planned_reports};
}
sub update_reputation {
diff --git a/perllib/FixMyStreet/DB/ResultSet/Body.pm b/perllib/FixMyStreet/DB/ResultSet/Body.pm
index 6802ed604..e79d038b1 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Body.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Body.pm
@@ -14,4 +14,15 @@ sub for_areas {
return $result;
}
+sub all_translated {
+ my $rs = shift;
+ my $schema = $rs->result_source->schema;
+ my @bodies = $rs->search(undef, {
+ '+columns' => { 'msgstr' => 'translations.msgstr' },
+ join => 'translations',
+ bind => [ 'name', $schema->lang, 'body' ],
+ })->all;
+ return @bodies;
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/Contact.pm b/perllib/FixMyStreet/DB/ResultSet/Contact.pm
index f402b5461..8ef6d1ac5 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Contact.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Contact.pm
@@ -10,13 +10,18 @@ sub me { join('.', shift->current_source_alias, shift || q{}) }
$rs = $rs->not_deleted();
-Filter down to not deleted contacts - which have C<deleted> set to false;
+Filter down to not deleted contacts (so active or inactive).
=cut
sub not_deleted {
my $rs = shift;
- return $rs->search( { $rs->me('deleted') => 0 } );
+ return $rs->search( { $rs->me('state') => { '!=' => 'deleted' } } );
+}
+
+sub active {
+ my $rs = shift;
+ $rs->search( { $rs->me('state') => [ 'unconfirmed', 'confirmed' ] } );
}
sub summary_count {
@@ -25,9 +30,9 @@ sub summary_count {
return $rs->search(
$restriction,
{
- group_by => ['confirmed'],
- select => [ 'confirmed', { count => 'id' } ],
- as => [qw/confirmed confirmed_count/]
+ group_by => ['state'],
+ select => [ 'state', { count => 'id' } ],
+ as => [qw/state state_count/]
}
);
}
diff --git a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm
index a873ef252..b2ef77f7c 100644
--- a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm
@@ -3,20 +3,26 @@ use base 'DBIx::Class::ResultSet';
use strict;
use warnings;
+use Moo;
+use HTML::Entities;
-sub for_bodies {
- my ($rs, $bodies, $category) = @_;
- my $attrs = {
- 'me.body_id' => $bodies,
- };
- if ($category) {
- $attrs->{'contact.category'} = [ $category, undef ];
- }
- $rs->search($attrs, {
- order_by => 'name',
- join => { 'contact_defect_types' => 'contact' },
- distinct => 1,
- });
+with('FixMyStreet::Roles::ContactExtra');
+
+sub join_table {
+ return 'contact_defect_types';
+}
+
+sub map_extras {
+ my ($rs, @ts) = @_;
+ return map {
+ my $meta = $_->get_extra_metadata();
+ my %extra = map { $_ => encode_entities($meta->{$_}) } keys %$meta;
+ {
+ id => $_->id,
+ name => encode_entities($_->name),
+ extra => \%extra
+ }
+ } @ts;
}
1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm b/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm
new file mode 100644
index 000000000..1348df3c2
--- /dev/null
+++ b/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm
@@ -0,0 +1,25 @@
+package FixMyStreet::DB::ResultSet::ReportExtraFields;
+use base 'DBIx::Class::ResultSet';
+
+use strict;
+use warnings;
+
+sub for_cobrand {
+ my ( $rs, $cobrand ) = @_;
+
+ my $result = $rs->search(
+ { cobrand => [ undef, $cobrand->moniker, '' ] }
+ );
+ return $result;
+}
+
+sub for_language {
+ my ( $rs, $language ) = @_;
+
+ my $result = $rs->search(
+ { language => [ undef, $language, '' ] }
+ );
+ return $result;
+}
+
+1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm b/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm
index aa9c426f4..89bb4dfd7 100644
--- a/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm
@@ -3,20 +3,18 @@ use base 'DBIx::Class::ResultSet';
use strict;
use warnings;
+use Moo;
+use HTML::Entities;
-sub for_bodies {
- my ($rs, $bodies, $category) = @_;
- my $attrs = {
- 'me.body_id' => $bodies,
- };
- if ($category) {
- $attrs->{'contact.category'} = [ $category, undef ];
- }
- $rs->search($attrs, {
- order_by => 'name',
- join => { 'contact_response_priorities' => 'contact' },
- distinct => 1,
- });
+with('FixMyStreet::Roles::ContactExtra');
+
+sub join_table {
+ return 'contact_response_priorities';
+}
+
+sub map_extras {
+ my ($rs, @ts) = @_;
+ return map { { id => $_->id, name => encode_entities($_->name) } } @ts;
}
1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm b/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm
new file mode 100644
index 000000000..aa070daa3
--- /dev/null
+++ b/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm
@@ -0,0 +1,27 @@
+package FixMyStreet::DB::ResultSet::ResponseTemplate;
+use base 'DBIx::Class::ResultSet';
+
+use Moo;
+use HTML::Entities;
+
+with('FixMyStreet::Roles::ContactExtra');
+
+sub join_table {
+ return 'contact_response_templates';
+}
+
+sub name_column {
+ 'title';
+}
+
+sub map_extras {
+ my ($rs, @ts) = @_;
+ return map {
+ my $out = { id => encode_entities($_->text), name => encode_entities($_->title) };
+ $out->{state} = encode_entities($_->state) if $_->state;
+ $out;
+ } @ts;
+}
+
+1;
+
diff --git a/perllib/FixMyStreet/DB/ResultSet/State.pm b/perllib/FixMyStreet/DB/ResultSet/State.pm
new file mode 100644
index 000000000..ac13ec2a4
--- /dev/null
+++ b/perllib/FixMyStreet/DB/ResultSet/State.pm
@@ -0,0 +1,84 @@
+package FixMyStreet::DB::ResultSet::State;
+use base 'DBIx::Class::ResultSet';
+
+use strict;
+use warnings;
+use Memcached;
+
+sub _hardcoded_states {
+ my $rs = shift;
+ # These are translated on use, not here
+ my $open = $rs->new({ id => -1, label => 'confirmed', type => 'open', name => "Open" });
+ my $closed = $rs->new({ id => -2, label => 'closed', type => 'closed', name => "Closed" });
+ return ($open, $closed);
+}
+
+# As states will change rarely, and then only through the admin,
+# we cache these in the package on first use, and clear on update.
+
+sub clear {
+ Memcached::set('states', '');
+}
+
+sub states {
+ my $rs = shift;
+
+ my $states = Memcached::get('states');
+ if ($states) {
+ # Need to reattach schema
+ $states->[0]->result_source->schema( $rs->result_source->schema ) if $states->[0];
+ return $states;
+ }
+
+ # Pick up and cache any translations
+ my $q = $rs->result_source->schema->resultset("Translation")->search({
+ tbl => 'state',
+ col => 'name',
+ });
+ my %trans;
+ $trans{$_->object_id}{$_->lang} = { id => $_->id, msgstr => $_->msgstr } foreach $q->all;
+
+ my @states = ($rs->_hardcoded_states, $rs->search(undef, { order_by => 'label' })->all);
+ $_->translated->{name} = $trans{$_->id} || {} foreach @states;
+ $states = \@states;
+ Memcached::set('states', $states);
+ return $states;
+}
+
+# Some functions to provide filters on the above data
+
+sub open { [ $_[0]->_filter(sub { $_->type eq 'open' }) ] }
+sub closed { [ $_[0]->_filter(sub { $_->type eq 'closed' }) ] }
+sub fixed { [ $_[0]->_filter(sub { $_->type eq 'fixed' }) ] }
+
+# We sometimes have only a state label to display, no associated object.
+# This function can be used to return that label's display name.
+
+sub display {
+ my ($rs, $label, $single_fixed) = @_;
+ my $unchanging = {
+ unconfirmed => _("Unconfirmed"),
+ hidden => _("Hidden"),
+ partial => _("Partial"),
+ 'fixed - council' => _("Fixed - Council"),
+ 'fixed - user' => _("Fixed - User"),
+ };
+ my $translate_now = {
+ confirmed => _("Open"),
+ closed => _("Closed"),
+ };
+ $label = 'fixed' if $single_fixed && $label =~ /^fixed - (council|user)$/;
+ return $unchanging->{$label} if $unchanging->{$label};
+ my ($state) = $rs->_filter(sub { $_->label eq $label });
+ return $label unless $state;
+ $state->name($translate_now->{$label}) if $translate_now->{$label};
+ return $state->msgstr;
+}
+
+sub _filter {
+ my ($rs, $fn) = @_;
+ my $states = $rs->states;
+ grep &$fn, @$states;
+}
+
+1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm b/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm
index 7e16e2dd3..460a4912e 100644
--- a/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm
@@ -6,7 +6,17 @@ use warnings;
sub active {
my $rs = shift;
- $rs->search({ removed => undef });
+
+ # If we have been prefetched we can't use `active` as that'll blow away the
+ # cache and query the DB due to the `removed IS NULL` clause. So let's do
+ # the filtering here instead, if the query has been prefetched.
+ if ( $rs->get_cache ) {
+ my @users = grep { !defined($_->removed) } $rs->all;
+ $rs->set_cache(\@users);
+ $rs;
+ } else {
+ $rs->search({ removed => undef });
+ }
}
sub for_report {
diff --git a/perllib/FixMyStreet/DB/Schema.pm b/perllib/FixMyStreet/DB/Schema.pm
new file mode 100644
index 000000000..be39069d8
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Schema.pm
@@ -0,0 +1,32 @@
+use utf8;
+package FixMyStreet::DB::Schema;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Schema';
+
+__PACKAGE__->load_namespaces(
+ result_namespace => "+FixMyStreet::DB::Result",
+ resultset_namespace => "+FixMyStreet::DB::ResultSet",
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-13 14:15:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UpH30RXb6SbCqRv2FPmpkg
+
+use Moo;
+use FixMyStreet;
+
+__PACKAGE__->connection(FixMyStreet->dbic_connect_info);
+
+has lang => ( is => 'rw' );
+
+has cobrand => ( is => 'rw' );
+
+has cache => ( is => 'rw', lazy => 1, default => sub { {} } );
+
+1;
diff --git a/perllib/FixMyStreet/Email/Sender.pm b/perllib/FixMyStreet/Email/Sender.pm
index e6148a56c..2fb819fbc 100644
--- a/perllib/FixMyStreet/Email/Sender.pm
+++ b/perllib/FixMyStreet/Email/Sender.pm
@@ -28,11 +28,14 @@ sub build_default_transport {
if ( FixMyStreet->test_mode ) {
Email::Sender::Util->easy_transport(Test => {});
} elsif ( my $smtp_host = FixMyStreet->config('SMTP_SMARTHOST') ) {
- my $type = FixMyStreet->config('SMTP_TYPE') || '';
+ my $type = lc (FixMyStreet->config('SMTP_TYPE') || '');
my $port = FixMyStreet->config('SMTP_PORT') || '';
my $username = FixMyStreet->config('SMTP_USERNAME') || '';
my $password = FixMyStreet->config('SMTP_PASSWORD') || '';
+ die "Bad SMTP_TYPE config: is $type, should be tls, ssl, or blank"
+ unless $type =~ /^(tls|ssl|)$/;
+
my $ssl = $type eq 'tls' ? 'starttls' : $type eq 'ssl' ? 'ssl' : '';
my $args = {
host => $smtp_host,
diff --git a/perllib/FixMyStreet/Integrations/ExorRDI.pm b/perllib/FixMyStreet/Integrations/ExorRDI.pm
new file mode 100644
index 000000000..093688e47
--- /dev/null
+++ b/perllib/FixMyStreet/Integrations/ExorRDI.pm
@@ -0,0 +1,243 @@
+package FixMyStreet::Integrations::ExorRDI::Error;
+
+use Moo;
+with 'Throwable';
+
+has message => (is => 'ro');
+
+package FixMyStreet::Integrations::ExorRDI::CSV;
+
+use parent 'Text::CSV';
+
+sub add_row {
+ my ($self, $data, @data) = @_;
+ $self->combine(@data);
+ push @$data, $self->string;
+}
+
+package FixMyStreet::Integrations::ExorRDI;
+
+use DateTime;
+use Moo;
+use Scalar::Util 'blessed';
+use FixMyStreet::DB;
+use namespace::clean;
+
+has [qw(start_date end_date inspection_date mark_as_processed)] => (
+ is => 'ro',
+ required => 1,
+);
+
+has user => (
+ is => 'ro',
+ coerce => sub {
+ return $_[0] if blessed($_[0]) && $_[0]->isa('FixMyStreet::DB::Result::User');
+ FixMyStreet::DB->resultset('User')->find( { id => $_[0] } )
+ if $_[0];
+ },
+);
+
+sub construct {
+ my $self = shift;
+
+ my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker('oxfordshire')->new;
+ my $dtf = $cobrand->problems->result_source->storage->datetime_parser;
+ my $now = DateTime->now(
+ time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone
+ );
+
+ my $missed_cutoff = $now - DateTime::Duration->new( hours => 24 );
+ my %params = (
+ -and => [
+ state => [ 'action scheduled' ],
+ external_id => { '!=' => undef },
+ -or => [
+ -and => [
+ 'admin_log_entries.action' => 'inspected',
+ 'admin_log_entries.whenedited' => { '>=', $dtf->format_datetime($self->start_date) },
+ 'admin_log_entries.whenedited' => { '<=', $dtf->format_datetime($self->end_date) },
+ ],
+ -and => [
+ extra => { -not_like => '%rdi_processed%' },
+ 'admin_log_entries.action' => 'inspected',
+ 'admin_log_entries.whenedited' => { '<=', $dtf->format_datetime($missed_cutoff) },
+ ]
+ ]
+ ]
+ );
+
+ $params{'admin_log_entries.user_id'} = $self->user->id if $self->user;
+
+ my $problems = $cobrand->problems->search(
+ \%params,
+ {
+ join => 'admin_log_entries',
+ distinct => 1,
+ }
+ );
+ FixMyStreet::Integrations::ExorRDI::Error->throw unless $problems->count;
+
+ # A single RDI file might contain inspections from multiple inspectors, so
+ # we need to group inspections by inspector within G records.
+ my $inspectors = {};
+ my $inspector_initials = {};
+ while ( my $report = $problems->next ) {
+ my $user = $report->inspection_log_entry->user;
+ $inspectors->{$user->id} ||= [];
+ push @{ $inspectors->{$user->id} }, $report;
+ unless ( $inspector_initials->{$user->id} ) {
+ $inspector_initials->{$user->id} = $user->get_extra_metadata('initials');
+ }
+ }
+
+ my $csv = FixMyStreet::Integrations::ExorRDI::CSV->new({ binary => 1, eol => "" });
+
+ my $p_count = 0;
+ my $link_id = $cobrand->exor_rdi_link_id;
+
+ # RDI first line is always the same
+ my $body = [];
+ $csv->add_row($body, "1", "1.8", "1.0.0.0", "ENHN", "");
+
+ my $i = 0;
+ foreach my $inspector_id (keys %$inspectors) {
+ my $inspections = $inspectors->{$inspector_id};
+ my $initials = $inspector_initials->{$inspector_id};
+
+ my %body_by_activity_code;
+ foreach my $report (@$inspections) {
+ my ($eastings, $northings) = $report->local_coords;
+
+ my $location = "${eastings}E ${northings}N";
+ $location = "[DID NOT USE MAP] $location" unless $report->used_map;
+ my $closest_address = $cobrand->find_closest($report, 1);
+ if (%$closest_address) {
+ $location .= " Nearest road: $closest_address->{road}." if $closest_address->{road};
+ $location .= " Nearest postcode: $closest_address->{postcode}{postcode}." if $closest_address->{postcode};
+ }
+
+ my $description = sprintf("%s %s", $report->external_id || "", $report->get_extra_metadata('detailed_information') || "");
+ my $activity_code = $report->defect_type ?
+ $report->defect_type->get_extra_metadata('activity_code')
+ : 'MC';
+ my $traffic_information = $report->get_extra_metadata('traffic_information') ?
+ 'TM ' . $report->get_extra_metadata('traffic_information')
+ : 'TM none';
+ $body_by_activity_code{$activity_code} ||= [];
+
+ $csv->add_row($body_by_activity_code{$activity_code},
+ "I", # beginning of defect record
+ $activity_code, # activity code - minor carriageway, also FC (footway)
+ "", # empty field, can also be A (seen on MC) or B (seen on FC)
+ sprintf("%03d", ++$i), # randomised sequence number
+ $location, # defect location field, which we don't capture from inspectors
+ $report->inspection_log_entry->whenedited->strftime("%H%M"), # defect time raised
+ "","","","","","","", # empty fields
+ $traffic_information,
+ $description, # defect description
+ );
+
+ my $defect_type = $report->defect_type ?
+ $report->defect_type->get_extra_metadata('defect_code')
+ : 'SFP2';
+ $csv->add_row($body_by_activity_code{$activity_code},
+ "J", # georeferencing record
+ $defect_type, # defect type - SFP2: sweep and fill <1m2, POT2 also seen
+ $report->response_priority ?
+ $report->response_priority->external_id :
+ "2", # priority of defect
+ "","", # empty fields
+ $eastings, # eastings
+ $northings, # northings
+ "","","","","" # empty fields
+ );
+
+ my $m_row_activity_code = $activity_code;
+ $m_row_activity_code .= 'I' if length $activity_code == 1;
+
+ $csv->add_row($body_by_activity_code{$activity_code},
+ "M", # bill of quantities record
+ "resolve", # permanent repair
+ "","", # empty fields
+ "/C$m_row_activity_code", # /C + activity code + perhaps an "I"
+ "", "" # empty fields
+ );
+ }
+
+ foreach my $activity_code (sort keys %body_by_activity_code) {
+ $csv->add_row($body,
+ "G", # start of an area/sequence
+ $link_id, # area/link id, fixed value for our purposes
+ "","", # must be empty
+ $initials || "XX", # inspector initials
+ $self->inspection_date->strftime("%y%m%d"), # date of inspection yymmdd
+ "1600", # time of inspection hhmm, set to static value for now
+ "D", # inspection variant, should always be D
+ "INS", # inspection type, always INS
+ "N", # Area of the county - north (N) or south (S)
+ "", "", "", "" # empty fields
+ );
+
+ $csv->add_row($body,
+ "H", # initial inspection type
+ $activity_code # e.g. MC = minor carriageway
+ );
+
+ # List of I/J/M entries from above
+ push @$body, @{$body_by_activity_code{$activity_code}};
+
+ # end this group of defects with a P record
+ $csv->add_row($body,
+ "P", # end of area/sequence
+ 0, # always 0
+ 999999, # charging code, always 999999 in OCC
+ );
+ $p_count++;
+ }
+ }
+
+ # end the RDI file with an X record
+ my $record_count = $i;
+ $csv->add_row($body,
+ "X", # end of inspection record
+ $p_count,
+ $p_count,
+ $record_count, # number of I records
+ $record_count, # number of J records
+ 0, 0, 0, # always zero
+ $record_count, # number of M records
+ 0, # always zero
+ $p_count,
+ 0, 0, 0 # error counts, always zero
+ );
+
+ if ($self->mark_as_processed) {
+ # Mark all these problems are having been included in an RDI
+ $problems->reset;
+ while ( my $report = $problems->next ) {
+ $report->set_extra_metadata('rdi_processed' => $now->strftime( '%Y-%m-%d %H:%M' ));
+ $report->update;
+ }
+ }
+
+ # The RDI format is very weird CSV - each line must be wrapped in
+ # double quotes.
+ return join "", map { "\"$_\"\r\n" } @$body;
+}
+
+has filename => (
+ is => 'lazy',
+ default => sub {
+ my $self = shift;
+ my $start = $self->inspection_date->strftime("%Y%m%d");
+ my $end = $self->end_date->strftime("%Y%m%d");
+ my $filename = sprintf("exor_defects-%s-%s.rdi", $start, $end);
+ if ( $self->user ) {
+ my $initials = $self->user->get_extra_metadata("initials") || "";
+ $filename = sprintf("exor_defects-%s-%s-%s.rdi", $start, $end, $initials);
+ }
+ return $filename;
+ },
+);
+
+1;
diff --git a/perllib/FixMyStreet/Roles/ContactExtra.pm b/perllib/FixMyStreet/Roles/ContactExtra.pm
new file mode 100644
index 000000000..55c055d99
--- /dev/null
+++ b/perllib/FixMyStreet/Roles/ContactExtra.pm
@@ -0,0 +1,48 @@
+package FixMyStreet::Roles::ContactExtra;
+
+use Moo::Role;
+use JSON::MaybeXS;
+
+requires 'join_table', 'map_extras';
+
+sub for_bodies {
+ my ($rs, $bodies, $category) = @_;
+ my $join_table = $rs->join_table();
+ my $attrs = {
+ 'me.body_id' => $bodies,
+ };
+ my $order = $rs->can('name_column') ? $rs->name_column() : 'name';
+ my $filters = {
+ order_by => $order,
+ join => { $join_table => 'contact' },
+ prefetch => $join_table,
+ distinct => 1,
+ };
+ if ($category) {
+ $attrs->{'contact.category'} = [ $category, undef ];
+ }
+ $rs->search($attrs, $filters);
+}
+
+sub by_categories {
+ my ($rs, $area_id, @contacts) = @_;
+ my %body_ids = map { $_->body_id => 1 } FixMyStreet::DB->resultset('BodyArea')->search({ area_id => $area_id });
+ my @body_ids = keys %body_ids;
+ my %extras = ();
+ my @results = $rs->for_bodies(\@body_ids, undef);
+ @contacts = grep { $body_ids{$_->body_id} } @contacts;
+
+ foreach my $contact (@contacts) {
+ my $join_table = $rs->join_table();
+ my @ts = grep {
+ $_->$join_table == 0 # There's no category at all on this defect type/template/priority
+ || (grep { $_->contact_id == $contact->get_column('id') } $_->$join_table)
+ } @results;
+ @ts = $rs->map_extras(@ts);
+ $extras{$contact->category} = encode_json(\@ts);
+ }
+
+ return \%extras;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Roles/Extra.pm b/perllib/FixMyStreet/Roles/Extra.pm
index dc2e5c241..445f6d91c 100644
--- a/perllib/FixMyStreet/Roles/Extra.pm
+++ b/perllib/FixMyStreet/Roles/Extra.pm
@@ -175,4 +175,20 @@ sub get_extra {
return $extra;
}
+=head2 get_extra_field_value
+
+Return the value of a field stored in `_fields` in extra, or undefined if
+it's not present.
+
+=cut
+
+sub get_extra_field_value {
+ my ($self, $name) = @_;
+
+ my @fields = @{ $self->get_extra_fields() };
+
+ my ($field) = grep { $_->{name} eq $name } @fields;
+ return $field->{value};
+}
+
1;
diff --git a/perllib/FixMyStreet/Roles/Translatable.pm b/perllib/FixMyStreet/Roles/Translatable.pm
new file mode 100644
index 000000000..d39d97bf8
--- /dev/null
+++ b/perllib/FixMyStreet/Roles/Translatable.pm
@@ -0,0 +1,116 @@
+package FixMyStreet::Roles::Translatable;
+
+use Moo::Role;
+use FixMyStreet;
+
+has _translated => (is => 'rw');
+
+sub translated {
+ my $self = shift;
+ $self->_translated or $self->_translated({});
+}
+
+sub translate_around {
+ my ($orig, $self) = (shift, shift);
+ my $fallback = $self->$orig(@_);
+ (my $col = (caller(2))[3]) =~ s/.*:://;
+ $self->_translate($col, $fallback);
+}
+
+sub translate_column {
+ my ($self, $col) = (shift, shift);
+ my $fallback = $self->$col(@_);
+ $self->_translate($col, $fallback);
+}
+
+sub _translate {
+ my ($self, $col, $fallback) = @_;
+
+ my $langs = FixMyStreet->config('LANGUAGES');
+ return $fallback if !$langs || @$langs < 2;
+
+ my %cols = $self->get_columns;
+ return $cols{msgstr} if $cols{msgstr};
+
+ my $schema = $self->result_source->schema;
+ my $table = lc $self->result_source->source_name;
+ my $id = $self->id;
+ my $lang = $schema->lang || '';
+
+ my $translated = $self->translated->{$col}{$lang};
+ return $translated if $translated;
+
+ # Deal with the fact problem table has denormalized copy of category string
+ if ($table eq 'problem' && $col eq 'category') {
+ my $body_id = $self->bodies_str_ids->[0];
+ return $fallback unless $body_id && $body_id =~ /^[0-9]+$/;
+ my $contact = $schema->resultset("Contact")->find( {
+ body_id => $body_id,
+ category => $fallback,
+ } );
+ return $fallback unless $contact; # Shouldn't happen, but some tests
+ $table = 'contact';
+ $id = $contact->id;
+ }
+
+ if (ref $schema) {
+ my $translation = $schema->resultset('Translation')->find({
+ lang => $lang,
+ tbl => $table,
+ object_id => $id,
+ col => $col
+ });
+ $fallback = $translation->msgstr if $translation;
+ } else {
+ warn "Can't use translation on this call to $table.$col";
+ }
+ $self->translated->{$col}{$lang} = $fallback;
+ return $fallback;
+};
+
+# These next two functions (translation_for and and_translation_for) are
+# convenience methods for use in the translation interface in the admin.
+# They shouldn't be used else where as they don't take account of things
+# like denormalised strings (e.g report category)
+sub translation_for {
+ my ($self, $col, $lang) = @_;
+
+ my $schema = $self->result_source->schema;
+
+ my $props = {
+ tbl => lc $self->result_source->source_name,
+ object_id => $self->id,
+ col => $col
+ };
+
+ if ($lang) {
+ $props->{lang} = $lang;
+ }
+
+ my $translations = $schema->resultset('Translation')->search($props);
+
+ return $lang ? $translations->first : $translations;
+}
+
+sub add_translation_for {
+ my ($self, $col, $lang, $msgstr) = @_;
+
+ my $schema = $self->result_source->schema;
+
+ my $props = {
+ tbl => lc $self->result_source->source_name,
+ object_id => $self->id,
+ col => $col,
+ lang => $lang,
+ msgstr => $msgstr,
+ };
+
+ my $translation = $schema->resultset('Translation')->update_or_create(
+ $props,
+ { key => 'translation_tbl_object_id_col_lang_key' }
+ );
+
+ return $translation;
+}
+
+1;
diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm
index 1a760a0c1..c001cc311 100644
--- a/perllib/FixMyStreet/Script/Alerts.pm
+++ b/perllib/FixMyStreet/Script/Alerts.pm
@@ -6,7 +6,6 @@ use warnings;
use DateTime::Format::Pg;
use IO::String;
-use mySociety::DBHandle qw(dbh);
use FixMyStreet::Gaze;
use mySociety::Locale;
use mySociety::MaPit;
@@ -18,8 +17,6 @@ use FixMyStreet::Email;
use FixMyStreet::Map;
use FixMyStreet::App::Model::PhotoSet;
-FixMyStreet->configure_mysociety_dbhandle;
-
my $parser = DateTime::Format::Pg->new();
# Child must have confirmed, id, email, state(!) columns
@@ -65,7 +62,7 @@ sub send() {
$query =~ s/\?/alert.parameter/ if ($query =~ /\?/);
$query =~ s/\?/alert.parameter2/ if ($query =~ /\?/);
- $query = dbh()->prepare($query);
+ $query = FixMyStreet::DB->schema->storage->dbh->prepare($query);
$query->execute();
my $last_alert_id;
my %data = ( template => $alert_type->template, data => [], schema => $schema );
@@ -105,7 +102,7 @@ sub send() {
my $url = $cobrand->base_url_for_report($row);
# this is currently only for new_updates
- if ($row->{item_text}) {
+ if (defined($row->{item_text})) {
if ( $cobrand->moniker ne 'zurich' && $row->{alert_user_id} == $row->{user_id} ) {
# This is an alert to the same user who made the report - make this a login link
# Don't bother with Zurich which has no accounts
@@ -143,7 +140,7 @@ sub send() {
# this is ward and council problems
} else {
if ( exists $row->{geocode} && $row->{geocode} && $ref =~ /ward|council/ ) {
- my $nearest_st = _get_address_from_gecode( $row->{geocode} );
+ my $nearest_st = _get_address_from_geocode( $row->{geocode} );
$row->{nearest} = $nearest_st;
}
@@ -228,7 +225,7 @@ sub send() {
and (select whenqueued from alert_sent where alert_sent.alert_id = ? and alert_sent.parameter::integer = problem.id) is null
and users.email <> ?
order by confirmed desc";
- $q = dbh()->prepare($q);
+ $q = FixMyStreet::DB->schema->storage->dbh->prepare($q);
$q->execute($latitude, $longitude, $d, $alert->whensubscribed, $alert->id, $alert->user->email);
while (my $row = $q->fetchrow_hashref) {
$schema->resultset('AlertSent')->create( {
@@ -236,7 +233,7 @@ sub send() {
parameter => $row->{id},
} );
if ( exists $row->{geocode} && $row->{geocode} ) {
- my $nearest_st = _get_address_from_gecode( $row->{geocode} );
+ my $nearest_st = _get_address_from_geocode( $row->{geocode} );
$row->{nearest} = $nearest_st;
}
my $dt = $parser->parse_timestamp( $row->{confirmed} );
@@ -304,7 +301,7 @@ sub _send_aggregated_alert_email(%) {
}
}
-sub _get_address_from_gecode {
+sub _get_address_from_geocode {
my $geocode = shift;
return '' unless defined $geocode;
diff --git a/perllib/FixMyStreet/Script/Questionnaires.pm b/perllib/FixMyStreet/Script/Questionnaires.pm
index 3f22eb150..ec6139d2d 100644
--- a/perllib/FixMyStreet/Script/Questionnaires.pm
+++ b/perllib/FixMyStreet/Script/Questionnaires.pm
@@ -16,6 +16,9 @@ sub send {
sub send_questionnaires_period {
my ( $period, $params ) = @_;
+ # Don't send if we don't have a fixed state
+ return unless FixMyStreet::DB::Result::Problem::fixed_states->{fixed};
+
my $rs = FixMyStreet::DB->resultset('Questionnaire');
# Select all problems that need a questionnaire email sending
diff --git a/perllib/FixMyStreet/Script/Reports.pm b/perllib/FixMyStreet/Script/Reports.pm
index 6057807de..1e5fd55bb 100644
--- a/perllib/FixMyStreet/Script/Reports.pm
+++ b/perllib/FixMyStreet/Script/Reports.pm
@@ -98,7 +98,7 @@ sub send(;$) {
$h{osm_url} = Utils::OpenStreetMap::short_url($h{latitude}, $h{longitude});
if ( $row->used_map ) {
- $h{closest_address} = $cobrand->find_closest( $h{latitude}, $h{longitude}, $row );
+ $h{closest_address} = $cobrand->find_closest($row);
$h{osm_url} .= '?m';
}
diff --git a/perllib/FixMyStreet/Script/UpdateAllReports.pm b/perllib/FixMyStreet/Script/UpdateAllReports.pm
new file mode 100755
index 000000000..1bd069ee8
--- /dev/null
+++ b/perllib/FixMyStreet/Script/UpdateAllReports.pm
@@ -0,0 +1,274 @@
+package FixMyStreet::Script::UpdateAllReports;
+
+use strict;
+use warnings;
+
+use FixMyStreet;
+use FixMyStreet::DB;
+
+use File::Path ();
+use File::Slurp;
+use JSON::MaybeXS;
+use List::MoreUtils qw(zip);
+use List::Util qw(sum);
+
+my $fourweeks = 4*7*24*60*60;
+
+# Age problems from when they're confirmed, except on Zurich
+# where they appear as soon as they're created.
+my $age_column = 'confirmed';
+if ( FixMyStreet->config('BASE_URL') =~ /zurich|zueri/ ) {
+ $age_column = 'created';
+}
+
+sub generate {
+ my $include_areas = shift;
+
+ my $problems = FixMyStreet::DB->resultset('Problem')->search(
+ {
+ state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
+ bodies_str => \'is not null',
+ },
+ {
+ columns => [
+ 'id', 'bodies_str', 'state', 'areas', 'cobrand',
+ { duration => { extract => "epoch from current_timestamp-lastupdate" } },
+ { age => { extract => "epoch from current_timestamp-$age_column" } },
+ ]
+ }
+ );
+ $problems = $problems->cursor; # Raw DB cursor for speed
+
+ my ( %fixed, %open );
+ my @cols = ( 'id', 'bodies_str', 'state', 'areas', 'cobrand', 'duration', 'age' );
+ while ( my @problem = $problems->next ) {
+ my %problem = zip @cols, @problem;
+ my @bodies;
+ my @areas;
+ my $cobrand = $problem{cobrand};
+ my $duration_str = ( $problem{duration} > 2 * $fourweeks ) ? 'old' : 'new';
+ my $type = ( $problem{duration} > 2 * $fourweeks )
+ ? 'unknown'
+ : ($problem{age} > $fourweeks ? 'older' : 'new');
+ my $problem_fixed =
+ FixMyStreet::DB::Result::Problem->fixed_states()->{$problem{state}}
+ || FixMyStreet::DB::Result::Problem->closed_states()->{$problem{state}};
+
+ # Add to bodies it was sent to
+ @bodies = split( /,/, $problem{bodies_str} );
+
+ foreach my $body ( @bodies ) {
+ if ( $problem_fixed ) {
+ # Fixed problems are either old or new
+ $fixed{$body}{$duration_str}++;
+ $fixed{$cobrand}{$body}{$duration_str}++;
+ } else {
+ # Open problems are either unknown, older, or new
+ $open{$body}{$type}++;
+ $open{$cobrand}{$body}{$type}++;
+ }
+ }
+
+ if ( $include_areas ) {
+ @areas = grep { $_ } split( /,/, $problem{areas} );
+ foreach my $area ( @areas ) {
+ if ( $problem_fixed ) {
+ $fixed{areas}{$area}{$duration_str}++;
+ } else {
+ $open{areas}{$area}{$type}++;
+ }
+ }
+ }
+ }
+
+ my $body = encode_json( {
+ fixed => \%fixed,
+ open => \%open,
+ } );
+
+ File::Path::mkpath( FixMyStreet->path_to( '../data/' )->stringify );
+ File::Slurp::write_file( FixMyStreet->path_to( '../data/all-reports.json' )->stringify, \$body );
+}
+
+sub end_period {
+ my $period = shift;
+ FixMyStreet->set_time_zone(DateTime->now)->truncate(to => $period)->add($period . 's' => 1)->subtract(seconds => 1);
+}
+
+sub loop_period {
+ my ($date, $period, $extra) = @_;
+ my $end = end_period($period);
+ my @out;
+ while ($date <= $end) {
+ push @out, { n => $date->$period, $extra ? (d => $date->$extra) : () };
+ $date->add($period . 's' => 1);
+ }
+ return @out;
+}
+
+sub generate_dashboard {
+ my %data;
+
+ my $end_today = end_period('day');
+ my $min_confirmed = FixMyStreet::DB->resultset('Problem')->search({
+ state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
+ }, {
+ select => [ { min => 'confirmed' } ],
+ as => [ 'confirmed' ],
+ })->first->confirmed;
+ if ($min_confirmed) {
+ $min_confirmed = $min_confirmed->truncate(to => 'day');
+ } else {
+ $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 %problems_reported_by_period = stuff_by_day_or_year(
+ $group_by, 'Problem',
+ state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
+ );
+ my %problems_fixed_by_period = stuff_by_day_or_year(
+ $group_by, 'Problem',
+ state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
+ );
+
+ my (@problems_reported_by_period, @problems_fixed_by_period);
+ foreach (map { $_->{n} } @problem_periods) {
+ push @problems_reported_by_period, ($problems_reported_by_period[-1]||0) + ($problems_reported_by_period{$_}||0);
+ push @problems_fixed_by_period, ($problems_fixed_by_period[-1]||0) + ($problems_fixed_by_period{$_}||0);
+ }
+ $data{problem_periods} = [ map { $_->{d} || $_->{n} } @problem_periods ];
+ $data{problems_reported_by_period} = \@problems_reported_by_period;
+ $data{problems_fixed_by_period} = \@problems_fixed_by_period;
+
+ my %last_seven_days = (
+ problems => [],
+ updated => [],
+ fixed => [],
+ );
+ $data{last_seven_days} = \%last_seven_days;
+
+ my $dtf = FixMyStreet::DB->schema->storage->datetime_parser;
+ my $eight_ago = $dtf->format_datetime(DateTime->now->subtract(days => 8));
+ %problems_reported_by_period = stuff_by_day_or_year('day',
+ 'Problem',
+ state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
+ confirmed => { '>=', $eight_ago },
+ );
+ %problems_fixed_by_period = stuff_by_day_or_year('day',
+ 'Comment',
+ confirmed => { '>=', $eight_ago },
+ -or => [
+ problem_state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
+ mark_fixed => 1,
+ ],
+ );
+ my %problems_updated_by_period = stuff_by_day_or_year('day',
+ 'Comment',
+ confirmed => { '>=', $eight_ago },
+ );
+
+ my $date = DateTime->today->subtract(days => 7);
+ while ($date < DateTime->today) {
+ push @{$last_seven_days{problems}}, $problems_reported_by_period{$date->day} || 0;
+ push @{$last_seven_days{fixed}}, $problems_fixed_by_period{$date->day} || 0;
+ push @{$last_seven_days{updated}}, $problems_updated_by_period{$date->day} || 0;
+ $date->add(days => 1);
+ }
+ $last_seven_days{problems_total} = sum @{$last_seven_days{problems}};
+ $last_seven_days{fixed_total} = sum @{$last_seven_days{fixed}};
+ $last_seven_days{updated_total} = sum @{$last_seven_days{updated}};
+
+ my(@top_five_bodies);
+ $data{top_five_bodies} = \@top_five_bodies;
+
+ my $bodies = FixMyStreet::DB->resultset('Body')->search;
+ 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)";
+ while (my $body = $bodies->next) {
+ my $subquery = FixMyStreet::DB->resultset('Comment')->to_body($body)->search({
+ -or => [
+ problem_state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
+ mark_fixed => 1,
+ ],
+ 'me.id' => \"= ($substmt)",
+ 'me.state' => 'confirmed',
+ }, {
+ select => [
+ { extract => "epoch from me.confirmed-problem.confirmed", -as => 'time' },
+ ],
+ as => [ qw/time/ ],
+ rows => 100,
+ order_by => { -desc => 'me.confirmed' },
+ join => 'problem'
+ })->as_subselect_rs;
+ my $avg = $subquery->search({
+ }, {
+ select => [ { avg => "time" } ],
+ as => [ qw/avg/ ],
+ })->first->get_column('avg');
+ push @top_five_bodies, { name => $body->name, days => int($avg / 60 / 60 / 24 + 0.5) }
+ if defined $avg;
+ }
+ @top_five_bodies = sort { $a->{days} <=> $b->{days} } @top_five_bodies;
+ $data{average} = @top_five_bodies
+ ? int((sum map { $_->{days} } @top_five_bodies) / @top_five_bodies + 0.5) : undef;
+
+ @top_five_bodies = @top_five_bodies[0..4] if @top_five_bodies > 5;
+
+ my $week_ago = $dtf->format_datetime(DateTime->now->subtract(days => 7));
+ my $last_seven_days = FixMyStreet::DB->resultset("Problem")->search({
+ confirmed => { '>=', $week_ago },
+ })->count;
+ my @top_five_categories = FixMyStreet::DB->resultset("Problem")->search({
+ confirmed => { '>=', $week_ago },
+ category => { '!=', 'Other' },
+ }, {
+ select => [ 'category', { count => 'id' } ],
+ as => [ 'category', 'count' ],
+ group_by => 'category',
+ rows => 5,
+ order_by => { -desc => 'count' },
+ });
+ $data{top_five_categories} = [ map {
+ { category => $_->category, count => $_->get_column('count') }
+ } @top_five_categories ];
+ foreach (@top_five_categories) {
+ $last_seven_days -= $_->get_column('count');
+ }
+ $data{other_categories} = $last_seven_days;
+
+ my $body = encode_json( \%data );
+ File::Path::mkpath( FixMyStreet->path_to( '../data/' )->stringify );
+ File::Slurp::write_file( FixMyStreet->path_to( '../data/all-reports-dashboard.json' )->stringify, \$body );
+}
+
+sub stuff_by_day_or_year {
+ my $period = shift;
+ my $table = shift;
+ my %params = @_;
+ my $results = FixMyStreet::DB->resultset($table)->search({
+ %params
+ }, {
+ select => [ { extract => \"$period from confirmed", -as => $period }, { count => 'id' } ],
+ as => [ $period, 'count' ],
+ group_by => [ $period ],
+ });
+ my %out;
+ while (my $row = $results->next) {
+ my $p = $row->get_column($period);
+ $out{$p} = $row->get_column('count');
+ }
+ return %out;
+}
+
+1;
diff --git a/perllib/FixMyStreet/SendReport/Angus.pm b/perllib/FixMyStreet/SendReport/Angus.pm
index cab5de173..b552fbd9d 100644
--- a/perllib/FixMyStreet/SendReport/Angus.pm
+++ b/perllib/FixMyStreet/SendReport/Angus.pm
@@ -7,7 +7,6 @@ BEGIN { extends 'FixMyStreet::SendReport'; }
use Try::Tiny;
use Encode;
use XML::Simple;
-use mySociety::Web qw(ent);
sub get_auth_token {
my ($self, $authxml) = @_;
diff --git a/perllib/FixMyStreet/SendReport/EastHants.pm b/perllib/FixMyStreet/SendReport/EastHants.pm
index 55ec79613..b24123f94 100644
--- a/perllib/FixMyStreet/SendReport/EastHants.pm
+++ b/perllib/FixMyStreet/SendReport/EastHants.pm
@@ -6,7 +6,7 @@ BEGIN { extends 'FixMyStreet::SendReport'; }
use Try::Tiny;
use Encode;
-use mySociety::Web qw(ent);
+use HTML::Entities;
sub construct_message {
my %h = @_;
@@ -43,8 +43,8 @@ sub send {
$eh_service ||= Integrations::EastHantsWSDL->on_fault(sub { my($soap, $res) = @_; die ref $res ? $res->faultstring : $soap->transport->status, "\n"; });
try {
# ServiceName, RemoteCreatedBy, Salutation, FirstName, Name, Email, Telephone, HouseNoName, Street, Town, County, Country, Postcode, Comments, FurtherInfo, ImageURL
- my $message = ent(encode_utf8($h->{message}));
- my $name = ent(encode_utf8($h->{name}));
+ my $message = encode_entities(encode_utf8($h->{message}));
+ my $name = encode_entities(encode_utf8($h->{name}));
my $result = $eh_service->INPUTFEEDBACK(
$h->{category}, 'FixMyStreet', '', '', $name, $h->{email}, $h->{phone},
'', '', '', '', '', '', $message, 'Yes', $h->{image_url}
diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm
index 28f3411d0..eefb14553 100644
--- a/perllib/FixMyStreet/SendReport/Email.pm
+++ b/perllib/FixMyStreet/SendReport/Email.pm
@@ -12,15 +12,14 @@ sub build_recipient_list {
my $all_confirmed = 1;
foreach my $body ( @{ $self->bodies } ) {
- my $contact = $row->result_source->schema->resultset("Contact")->find( {
- deleted => 0,
+ my $contact = $row->result_source->schema->resultset("Contact")->not_deleted->find( {
body_id => $body->id,
category => $row->category
} );
- my ($body_email, $confirmed, $note) = ( $contact->email, $contact->confirmed, $contact->note );
+ my ($body_email, $state, $note) = ( $contact->email, $contact->state, $contact->note );
- unless ($confirmed) {
+ unless ($state eq 'confirmed') {
$all_confirmed = 0;
$note = 'Body ' . $row->bodies_str . ' deleted'
unless $note;
diff --git a/perllib/FixMyStreet/SendReport/Open311.pm b/perllib/FixMyStreet/SendReport/Open311.pm
index 059690612..eaa223bb2 100644
--- a/perllib/FixMyStreet/SendReport/Open311.pm
+++ b/perllib/FixMyStreet/SendReport/Open311.pm
@@ -35,8 +35,7 @@ sub send {
# Try and fill in some ones that we've been asked for, but not asked the user for
- my $contact = $row->result_source->schema->resultset("Contact")->find( {
- deleted => 0,
+ my $contact = $row->result_source->schema->resultset("Contact")->not_deleted->find( {
body_id => $body->id,
category => $row->category
} );
diff --git a/perllib/FixMyStreet/Test.pm b/perllib/FixMyStreet/Test.pm
new file mode 100644
index 000000000..572ae0a44
--- /dev/null
+++ b/perllib/FixMyStreet/Test.pm
@@ -0,0 +1,32 @@
+package FixMyStreet::Test;
+
+use parent qw(Exporter);
+
+use strict;
+use warnings FATAL => 'all';
+use utf8;
+use Test::More;
+use mySociety::Locale;
+use FixMyStreet::DB;
+
+my $db = FixMyStreet::DB->schema->storage;
+
+sub import {
+ strict->import;
+ warnings->import(FATAL => 'all');
+ utf8->import;
+ Test::More->export_to_level(1);
+ $db->txn_begin;
+}
+
+BEGIN {
+ use FixMyStreet;
+ FixMyStreet->test_mode(1);
+ mySociety::Locale::gettext_domain('FixMyStreet', 1);
+}
+
+END {
+ $db->txn_rollback if $db;
+}
+
+1;
diff --git a/perllib/FixMyStreet/TestAppProve.pm b/perllib/FixMyStreet/TestAppProve.pm
index 5298d225f..7a387547d 100644
--- a/perllib/FixMyStreet/TestAppProve.pm
+++ b/perllib/FixMyStreet/TestAppProve.pm
@@ -21,7 +21,7 @@ see bin/run-tests for usage
=cut
sub cleanup {
- unlink "conf/general.test-autogenerated.$$.yml";
+ unlink "conf/general-test-autogenerated.$$.yml";
}
sub signal_handler {
@@ -75,7 +75,7 @@ sub run {
$SIG{__WARN__} =
sub { print STDERR @_ if $_[0] !~ m/NOTICE: CREATE TABLE/; };
$dbh->do( path('db/schema.sql')->slurp ) or die $!;
- $dbh->do( path('db/alert_types.sql')->slurp ) or die $!;
+ $dbh->do( path('db/fixture.sql')->slurp ) or die $!;
$dbh->do( path('db/generate_secret.sql')->slurp ) or die $!;
$SIG{__WARN__} = $tmpwarn;
@@ -86,23 +86,23 @@ sub run {
$config->{FMS_DB_PASS} = '';
}
- my $config_out = "general.test-autogenerated.$$";
+ my $config_out = "general-test-autogenerated.$$";
path("conf/$config_out.yml")->spew( YAML::Dump($config) );
local $ENV{FMS_OVERRIDE_CONFIG} = $config_out;
- # If no arguments, test everything
- unshift @ARGV, 't' unless @ARGV;
+ my $prove = App::Prove->new;
+ $prove->process_args(@ARGV);
+ # If no arguments, test everything
+ $prove->argv(['t']) unless @{$prove->argv};
# verbose if we have a single file
- unshift @ARGV, '--verbose' if @ARGV and -f $ARGV[-1];
+ $prove->verbose(1) if @{$prove->argv} and -f $prove->argv->[-1];
+ # we always want to recurse
+ $prove->recurse(1);
+ # we always want to save state
+ $prove->state([ @state, 'save' ]);
- unshift @ARGV,
- '--recurse', # we always want to recurse
- '--state', (join ',' => @state, 'save'); # we always want to save state
-
- my $prove = App::Prove->new;
- $prove->process_args(@ARGV);
$prove->run;
}
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index 166ba116f..46f5344e2 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -1,12 +1,13 @@
package FixMyStreet::TestMech;
-use base qw(Test::WWW::Mechanize::Catalyst Test::Builder::Module);
+use parent qw(Test::WWW::Mechanize::Catalyst Test::Builder::Module);
-use strict;
-use warnings;
+use FixMyStreet::Test;
-BEGIN {
- use FixMyStreet;
- FixMyStreet->test_mode(1);
+sub import {
+ strict->import;
+ warnings->import(FATAL => 'all');
+ utf8->import;
+ Test::More->export_to_level(1);
}
use Test::WWW::Mechanize::Catalyst 'FixMyStreet::App';
@@ -484,31 +485,6 @@ sub extract_problem_list {
return $result->{ problems } || [];
}
-=head2 extract_report_stats
-
- $stats = $mech->extract_report_stats
-
-Returns a hash ref keyed by council name of all the council stats from the all reports
-page. Each value is an array ref with the first element being the council name and the
-rest being the stats in the order the appear in each row.
-
-=cut
-
-sub extract_report_stats {
- my $mech = shift;
-
- my $result = scraper {
- process 'tr[align=center]', 'councils[]' => scraper {
- process 'td.title a', 'council', 'TEXT',
- process 'td', 'stats[]', 'TEXT'
- }
- }->scrape( $mech->response );
-
- my %councils = map { $_->{council} => $_->{stats} } @{ $result->{councils} };
-
- return \%councils;
-}
-
=head2 visible_form_values
$hashref = $mech->visible_form_values( );
@@ -655,8 +631,7 @@ sub delete_defect_type {
sub create_contact_ok {
my $self = shift;
my %contact_params = (
- confirmed => 1,
- deleted => 0,
+ state => 'confirmed',
editor => 'Test',
whenedited => \'current_timestamp',
note => 'Created for test',
@@ -668,16 +643,12 @@ sub create_contact_ok {
}
sub create_body_ok {
- my $self = shift;
- my ( $area_id, $name, %extra ) = @_;
+ my ( $self, $area_id, $name, $params ) = @_;
+
+ $params->{name} = $name;
my $body = FixMyStreet::DB->resultset('Body');
- my $params = { name => $name };
- if ($extra{id}) {
- $body = $body->update_or_create({ %$params, id => $extra{id} }, { key => 'primary' });
- } else {
- $body = $body->find_or_create($params);
- }
+ $body = $body->find_or_create( $params );
ok $body, "found/created body $name";
$body->body_areas->delete;