diff options
Diffstat (limited to 'perllib')
89 files changed, 2873 insertions, 1115 deletions
diff --git a/perllib/Catalyst/Plugin/Compress/Gzip.pm b/perllib/Catalyst/Plugin/Compress/Gzip.pm deleted file mode 100644 index 06532c84c..000000000 --- a/perllib/Catalyst/Plugin/Compress/Gzip.pm +++ /dev/null @@ -1,82 +0,0 @@ -package Catalyst::Plugin::Compress::Gzip; -use strict; -use warnings; -use MRO::Compat; - -use Compress::Zlib (); - -sub finalize_headers { - my $c = shift; - - if ( $c->response->content_encoding ) { - return $c->next::method(@_); - } - - unless ( $c->response->body ) { - return $c->next::method(@_); - } - - unless ( $c->response->status == 200 ) { - return $c->next::method(@_); - } - - unless ( $c->response->content_type =~ /^text|xml$|javascript$/ ) { - return $c->next::method(@_); - } - - my $accept = $c->request->header('Accept-Encoding') || ''; - - unless ( index( $accept, "gzip" ) >= 0 ) { - return $c->next::method(@_); - } - - - my $body = $c->response->body; - eval { local $/; $body = <$body> } if ref $body; - die "Response body is an unsupported kind of reference" if ref $body; - - $c->response->body( Compress::Zlib::memGzip( $body ) ); - $c->response->content_length( length( $c->response->body ) ); - $c->response->content_encoding('gzip'); - $c->response->headers->push_header( 'Vary', 'Accept-Encoding' ); - - $c->next::method(@_); -} - -1; - -__END__ - -=head1 NAME - -Catalyst::Plugin::Compress::Gzip - Gzip response - -=head1 SYNOPSIS - - use Catalyst qw[Compress::Gzip]; - - -=head1 DESCRIPTION - -Gzip compress response if client supports it. Changed from CPAN version to -overload finalize_headers, rather than finalize. - -=head1 METHODS - -=head2 finalize_headers - -=head1 SEE ALSO - -L<Catalyst>. - -=head1 AUTHOR - -Christian Hansen, C<ch@ngmedia.com> -Matthew Somerville. - -=head1 LICENSE - -This library is free software . You can redistribute it and/or modify it under -the same terms as perl itself. - -=cut diff --git a/perllib/Catalyst/TraitFor/Model/DBIC/Schema/QueryLog/AdoptPlack.pm b/perllib/Catalyst/TraitFor/Model/DBIC/Schema/QueryLog/AdoptPlack.pm new file mode 100644 index 000000000..22509568e --- /dev/null +++ b/perllib/Catalyst/TraitFor/Model/DBIC/Schema/QueryLog/AdoptPlack.pm @@ -0,0 +1,128 @@ +# Local version to clone schema in enable_dbic_querylogging + +package Catalyst::TraitFor::Model::DBIC::Schema::QueryLog::AdoptPlack; +our $VERSION = "0.07"; + +use 5.008004; +use Moose::Role; +use Plack::Middleware::DBIC::QueryLog; +use Scalar::Util 'blessed'; + +with 'Catalyst::Component::InstancePerContext'; + +requires 'storage'; + +has show_missing_ql_warning => (is=>'rw', default=>1); + +sub get_querylog_from_env { + my ($self, $env) = @_; + return Plack::Middleware::DBIC::QueryLog->get_querylog_from_env($env); +} + +sub infer_env_from { + my ($self, $ctx) = @_; + if($ctx->engine->can('env')) { + return $ctx->engine->env; + } elsif($ctx->request->can('env')) { + return $ctx->request->env; + } else { return } +} + +sub enable_dbic_querylogging { + my ($self, $querylog) = @_; + my $clone = $self->clone; + $clone->storage->debugobj($querylog); + $clone->storage->debug(1); +} + +sub die_missing_querylog { + shift->show_missing_ql_warning(0); + die <<DEAD; +You asked me to querylog DBIC, but there is no querylog object in the Plack +\$env. You probably forgot to enable Plack::Middleware::Debug::DBIC::QueryLog +in your debugging panel. +DEAD +} + +sub die_not_plack { + die "Not a Plack Engine or compatible interface!" +} + +sub build_per_context_instance { + my ( $self, $ctx ) = @_; + return $self unless blessed($ctx); + + if(my $env = $self->infer_env_from($ctx)) { + if(my $querylog = $self->get_querylog_from_env($env)) { + $self->enable_dbic_querylogging($querylog); + } else { + $self->die_missing_querylog() if + $self->show_missing_ql_warning; + } + } else { + die_not_plack(); + } + + return $self; +} + +1; + +=head1 NAME + +Catalyst::TraitFor::Model::DBIC::Schema::QueryLog::AdoptPlack - Use a Plack Middleware QueryLog + +=head1 SYNOPSIS + + package MyApp::Web::Model::Schema; + use parent 'Catalyst::Model::DBIC::Schema'; + + __PACKAGE__->config({ + schema_class => 'MyApp::Schema', + traits => ['QueryLog::AdoptPlack'], + ## .. rest of configuration + }); + +=head1 DESCRIPTION + +This is a trait for L<Catalyst::Model::DBIC::Schema> which adopts a L<Plack> +created L<DBIx::Class::QueryLog> and logs SQL for a given request cycle. It is +intended to be compatible with L<Catalyst::TraitFor::Model::DBIC::Schema::QueryLog> +which you may already be using. + +It picks up the querylog from C<< $env->{'plack.middleware.dbic.querylog'} >> +or from C<< $env->{'plack.middleware.debug.dbic.querylog'} >> which is generally +provided by the L<Plack> middleware L<Plack::Middleware::Debug::DBIC::QueryLog> +In fact you will probably use these two modules together. Please see the documentation +in L<Plack::Middleware::Debug::DBIC::QueryLog> for an example. + +PLEASE NOTE: Starting with the 0.04 version of L<Plack::Middleware::Debug::DBIC::QueryLog> +we will canonicalize on C<< $env->{'plack.middleware.dbic.querylog'} >>. For now +both listed keys will work, but within a release or two the older key will warn and +prompt you to upgrade your version of L<Plack::Middleware::Debug::DBIC::QueryLog>. +Sorry for the trouble. + +=head1 SEE ALSO + +L<Plack::Middleware::Debug::DBIC::QueryLog>, +L<Catalyst::TraitFor::Model::DBIC::Schema::QueryLog>, +L<Catalyst::Model::DBIC::Schema>, +L<Plack::Middleware::Debug> + +=head1 ACKNOWLEGEMENTS + +This code inspired from L<Catalyst::TraitFor::Model::DBIC::Schema::QueryLog> +and the author owes a debt of gratitude for the original authors. + +=head1 AUTHOR + +John Napiorkowski, C<< <jjnapiork@cpan.org> >> + +=head1 COPYRIGHT & LICENSE + +Copyright 2012, John Napiorkowski + +This program is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/perllib/FixMyStreet.pm b/perllib/FixMyStreet.pm index 1f4579293..b3d963074 100644 --- a/perllib/FixMyStreet.pm +++ b/perllib/FixMyStreet.pm @@ -11,7 +11,6 @@ use Readonly; use Sub::Override; use mySociety::Config; -use mySociety::DBHandle; my $CONF_FILE = $ENV{FMS_OVERRIDE_CONFIG} || 'general'; @@ -135,9 +134,6 @@ Most of the values are read from the config file and others are hordcoded here. # # we use the one that is most similar to DBI's connect. -# FIXME - should we just use mySociety::DBHandle? will that lead to AutoCommit -# woes (we want it on, it sets it to off)? - sub dbic_connect_info { my $class = shift; my $config = $class->config; @@ -161,35 +157,6 @@ sub dbic_connect_info { return ( $dsn, $user, $password, $dbi_args, $dbic_args ); } -=head2 configure_mysociety_dbhandle - - FixMyStreet->configure_mysociety_dbhandle(); - -Calls configure in mySociety::DBHandle with args from the config. We need to do -this so that old code that uses mySociety::DBHandle finds it properly set up. We -can't (might not) be able to share the handle as DBIx::Class wants it with -AutoCommit on (so that its transaction code can be used in preference to calling -begin and commit manually) and mySociety::* code does not. - -This should be fixed/standardized to avoid having two database handles floating -around. - -=cut - -sub configure_mysociety_dbhandle { - my $class = shift; - my $config = $class->config; - - mySociety::DBHandle::configure( - Name => $config->{FMS_DB_NAME}, - User => $config->{FMS_DB_USER}, - Password => $config->{FMS_DB_PASS}, - Host => $config->{FMS_DB_HOST} || undef, - Port => $config->{FMS_DB_PORT} || undef, - ); - -} - my $tz; my $tz_f; @@ -210,6 +177,7 @@ sub set_time_zone { my $tz_f = time_zone(); $dt->set_time_zone($tz); $dt->set_time_zone($tz_f) if $tz_f; + return $dt; } # Development functions 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; diff --git a/perllib/Open311.pm b/perllib/Open311.pm index c1323acfa..da5a0a377 100644 --- a/perllib/Open311.pm +++ b/perllib/Open311.pm @@ -59,6 +59,24 @@ sub get_service_meta_info { return $self->_get_xml_object( $service_meta_xml ); } +sub to_bristol { + my $problem = shift; + return unless $problem->cobrand =~ /fixmystreet|bristol/; + my $bodies = $problem->bodies; + return unless %$bodies; + my $body = (values %$bodies)[0]; + return unless $body->areas->{2561}; + return 1; +} + +sub warn_failure { + my ($obj, $problem) = @_; + # Special case a poorly behaving Open311 server + return 0 if to_bristol($problem || $obj); + my $threshold = 1; + return $obj->send_fail_count && $obj->send_fail_count == $threshold; +} + sub send_service_request { my $self = shift; my $problem = shift; @@ -83,10 +101,10 @@ sub send_service_request { } warn sprintf( "Failed to submit problem %s over Open311, response\n: %s\n%s", $problem->id, $response, $self->debug_details ) - if $problem->send_fail_count && $problem->send_fail_count == 2; + if warn_failure($problem); } else { warn sprintf( "Failed to submit problem %s over Open311, details:\n%s", $problem->id, $self->error) - if $problem->send_fail_count && $problem->send_fail_count == 2; + if warn_failure($problem); } return 0; } @@ -175,6 +193,7 @@ sub _generate_service_request_description { $description = "title: " . $problem->title . "\n\n$description"; } } elsif ($problem->cobrand eq 'fixamingata') { + $description .= "Titel: " . $problem->title . "\n\n"; $description .= "Beskrivning: " . $problem->detail . "\n\n"; $description .= "Länk till ärendet: " . $extra->{url} . "\n\n"; $description .= "Skickad via FixaMinGata\n"; @@ -259,10 +278,10 @@ sub post_service_request_update { } warn sprintf( "Failed to submit comment %s over Open311, response - %s\n%s\n", $comment->id, $response, $self->debug_details ) - if $comment->send_fail_count && $comment->send_fail_count == 2; + if warn_failure($comment, $comment->problem); } else { warn sprintf( "Failed to submit comment %s over Open311, details\n%s\n", $comment->id, $self->error) - if $comment->send_fail_count && $comment->send_fail_count == 2; + if warn_failure($comment, $comment->problem); } return 0; } diff --git a/perllib/Open311/GetServiceRequestUpdates.pm b/perllib/Open311/GetServiceRequestUpdates.pm index 0751092fb..db2a452da 100644 --- a/perllib/Open311/GetServiceRequestUpdates.pm +++ b/perllib/Open311/GetServiceRequestUpdates.pm @@ -11,7 +11,7 @@ has start_date => ( is => 'ro', default => sub { undef } ); has end_date => ( is => 'ro', default => sub { undef } ); has suppress_alerts => ( is => 'rw', default => 0 ); has verbose => ( is => 'ro', default => 0 ); -has schema => ( is =>'ro', lazy => 1, default => sub { FixMyStreet::DB->connect } ); +has schema => ( is =>'ro', lazy => 1, default => sub { FixMyStreet::DB->schema->connect } ); Readonly::Scalar my $AREA_ID_BROMLEY => 2482; Readonly::Scalar my $AREA_ID_OXFORDSHIRE => 2237; @@ -142,7 +142,10 @@ sub update_comments { # don't update state unless it's an allowed state and it's # actually changing the state of the problem - if ( FixMyStreet::DB::Result::Problem->council_states()->{$state} && $p->state ne $state && + if ( FixMyStreet::DB::Result::Problem->visible_states()->{$state} && $p->state ne $state && + # For Oxfordshire, don't allow changes back to Open from other open states + !( $body->areas->{$AREA_ID_OXFORDSHIRE} && $state eq 'confirmed' && $p->is_open ) && + # Don't let it change between the (same in the front end) fixed states !( $p->is_fixed && FixMyStreet::DB::Result::Problem->fixed_states()->{$state} ) ) { if ($p->is_visible) { $p->state($state); diff --git a/perllib/Open311/PopulateServiceList.pm b/perllib/Open311/PopulateServiceList.pm index c5f17334b..764207626 100644 --- a/perllib/Open311/PopulateServiceList.pm +++ b/perllib/Open311/PopulateServiceList.pm @@ -6,7 +6,7 @@ use Open311; has bodies => ( is => 'ro' ); has found_contacts => ( is => 'rw', default => sub { [] } ); has verbose => ( is => 'ro', default => 0 ); -has schema => ( is => 'ro', lazy => 1, default => sub { FixMyStreet::DB->connect } ); +has schema => ( is => 'ro', lazy => 1, default => sub { FixMyStreet::DB->schema->connect } ); has _current_body => ( is => 'rw' ); has _current_open311 => ( is => 'rw' ); @@ -131,14 +131,13 @@ sub _handle_existing_contact { print $self->_current_body->id . " already has a contact for service code " . $self->_current_service->{service_code} . "\n" if $self->verbose >= 2; - if ( $contact->deleted || $service_name ne $contact->category || $self->_current_service->{service_code} ne $contact->email ) { + if ( $contact->state eq 'deleted' || $service_name ne $contact->category || $self->_current_service->{service_code} ne $contact->email ) { eval { $contact->update( { category => $service_name, email => $self->_current_service->{service_code}, - confirmed => 1, - deleted => 0, + state => 'confirmed', editor => $0, whenedited => \'current_timestamp', note => 'automatically undeleted by script', @@ -175,8 +174,7 @@ sub _create_contact { email => $self->_current_service->{service_code}, body_id => $self->_current_body->id, category => $service_name, - confirmed => 1, - deleted => 0, + state => 'confirmed', editor => $0, whenedited => \'current_timestamp', note => 'created automatically by script', @@ -278,11 +276,10 @@ sub _normalize_service_name { sub _delete_contacts_not_in_service_list { my $self = shift; - my $found_contacts = $self->schema->resultset('Contact')->search( + my $found_contacts = $self->schema->resultset('Contact')->not_deleted->search( { email => { -not_in => $self->found_contacts }, body_id => $self->_current_body->id, - deleted => 0, } ); @@ -299,7 +296,7 @@ sub _delete_contacts_not_in_service_list { $found_contacts->update( { - deleted => 1, + state => 'deleted', editor => $0, whenedited => \'current_timestamp', note => 'automatically marked as deleted by script' diff --git a/perllib/Plack/Middleware/Debug/FixMyStreet/Template.pm b/perllib/Plack/Middleware/Debug/FixMyStreet/Template.pm new file mode 100644 index 000000000..05cb9f18a --- /dev/null +++ b/perllib/Plack/Middleware/Debug/FixMyStreet/Template.pm @@ -0,0 +1,33 @@ +package Plack::Middleware::Debug::FixMyStreet::Template; + +=head1 NAME + +Plack::Middleware::Debug::FixMyStreet::Template - +small subclass for FixMyStreet-specific tweaks. + +=head1 VERSION + +Version 1.00 + +=cut + +our $VERSION = '1.00'; + +use strict; +use warnings; +use parent qw(Plack::Middleware::Debug::Template); + +sub show_pathname { 1 } + +sub hook_pathname { + my ($self, $name) = @_; + $name =~ s/^.*templates\/web\///; + $name; +} + +sub ignore_template { + my ($self, $template) = @_; + return 1 if $template eq 'site-name.html'; +} + +1; |