diff options
Diffstat (limited to 'perllib')
96 files changed, 5916 insertions, 1502 deletions
diff --git a/perllib/Catalyst/Plugin/Compress/Gzip.pm b/perllib/Catalyst/Plugin/Compress/Gzip.pm new file mode 100644 index 000000000..06532c84c --- /dev/null +++ b/perllib/Catalyst/Plugin/Compress/Gzip.pm @@ -0,0 +1,82 @@ +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/CronFns.pm b/perllib/CronFns.pm index 0573b788d..ac7f85596 100755 --- a/perllib/CronFns.pm +++ b/perllib/CronFns.pm @@ -24,16 +24,19 @@ sub options { die "Either no arguments, --nomail or --verbose" if (@ARGV>1); my $nomail = 0; my $verbose = 0; + my $debug = 0; $nomail = 1 if (@ARGV==1 && $ARGV[0] eq '--nomail'); $verbose = 1 if (@ARGV==1 && $ARGV[0] eq '--verbose'); + $debug = 1 if (@ARGV==1 && $ARGV[0] eq '--debug'); $verbose = 1 if $nomail; - return ($verbose, $nomail); + return ($verbose, $nomail, $debug); } sub site { my $base_url = shift; my $site = 'fixmystreet'; $site = 'emptyhomes' if $base_url =~ 'emptyhomes'; + $site = 'zurich' if $base_url =~ /zurich|zueri/; return $site; } diff --git a/perllib/CrossSell.pm b/perllib/CrossSell.pm index f9cde6936..f9bb73d6a 100644 --- a/perllib/CrossSell.pm +++ b/perllib/CrossSell.pm @@ -142,6 +142,49 @@ details. You can unsubscribe at any time.</p> EOF } +sub display_survey_link { + return <<EOF; +<h1 style="padding-top:0.5em">User Survey</h1> +<p> +We're running a survey to help us understand who uses our sites. If you have 10-15 minutes to spare then we'd be grateful if you could <a href="http://questions.mysociety.org/S/fms/w/" target="_blank">take part</a>. +</p> +EOF +} + +sub display_wtt_link { + return <<EOF; +<h1 style="padding-top:0.5em">WriteToThem</h1> +<p> +Need to write to a politician? Try <a href="https://writetothem.com">WriteToThem</a> - great +for campaigns too. +</p> +EOF +} + +sub display_app_links { + return <<EOF; +<h1 style="padding-top:0.5em">New! FixMyStreet Apps for Apple and Android</h1> +<p> +We've built all-new FixMyStreet phone apps, for your use. Key +improvements include offline reporting (for where there is bad signal) +and a better interface for adding problem reports more quickly and +easily. Please give the apps a go and tell us what you think. +</p> + +<p> +<a href="https://play.google.com/store/apps/details?id=org.mysociety.FixMyStreet"> + <img alt="FixMyStreet Android app on Google Play" + src="/cobrands/fixmystreet/images/google_play_logo.png" /> +</a> + +<a href="https://itunes.apple.com/gb/app/fixmystreet/id297456545"> + <img alt="FixMyStreet app on the App Store" + src="/cobrands/fixmystreet/images/itunes_store_logo.png" /> +</a><br> +</p> +EOF +} + # Not currently used, needs more explanation and testing; perhaps in future. sub display_gny_groups { my ($lon, $lat) = @_; @@ -201,6 +244,11 @@ sub display_advert ($$;$%) { #EOF #unless (defined $data{done_tms} && $data{done_tms}==1) { + $c->stash->{scratch} = 'advert=wtt'; + return '<div style="margin: 0 5em; border-top: dotted 1px #666666;">' + . display_app_links() + . '</div>'; + $c->stash->{scratch} = 'advert=news'; my $auth_signature = ''; unless (defined $data{emailunvalidated} && $data{emailunvalidated}==1) { diff --git a/perllib/FixMyStreet.pm b/perllib/FixMyStreet.pm index 2d8f462d5..de55e0070 100644 --- a/perllib/FixMyStreet.pm +++ b/perllib/FixMyStreet.pm @@ -7,12 +7,15 @@ use Path::Class; my $ROOT_DIR = file(__FILE__)->parent->parent->absolute->resolve; use Readonly; +use Sub::Override; use mySociety::Config; use mySociety::DBHandle; +my $CONF_FILE = $ENV{FMS_OVERRIDE_CONFIG} || 'general'; + # load the config file and store the contents in a readonly hash -mySociety::Config::set_file( __PACKAGE__->path_to("conf/general") ); +mySociety::Config::set_file( __PACKAGE__->path_to("conf/${CONF_FILE}") ); Readonly::Hash my %CONFIG, %{ mySociety::Config::get_list() }; =head1 NAME @@ -33,7 +36,7 @@ Thus module has utility functions for the FMS project. FixMyStreet->test_mode( $bool ); my $in_test_mode_bool = FixMyStreet->test_mode; -Put the FixMyStreet into test mode - inteded for the unit tests: +Put the FixMyStreet into test mode - intended for the unit tests: BEGIN { use FixMyStreet; @@ -85,6 +88,42 @@ sub config { return exists $CONFIG{$key} ? $CONFIG{$key} : undef; } +sub override_config($&) { + my $config = shift; + my $code = \&{shift @_}; + + mySociety::MaPit::configure($config->{MAPIT_URL}) if $config->{MAPIT_URL}; + + # For historical reasons, we have two ways of asking for config variables. + # Override them both, I'm sure we'll find time to get rid of one eventually. + my $override_guard1 = Sub::Override->new( + "FixMyStreet::config", + sub { + my ($class, $key) = @_; + return { %CONFIG, %$config } unless $key; + return $config->{$key} if exists $config->{$key}; + my $orig_config = mySociety::Config::load_default(); + return $orig_config->{$key} if exists $orig_config->{$key}; + } + ); + my $override_guard2 = Sub::Override->new( + "mySociety::Config::get", + sub ($;$) { + my ($key, $default) = @_; + return $config->{$key} if exists $config->{$key}; + my $orig_config = mySociety::Config::load_default(); + return $orig_config->{$key} if exists $orig_config->{$key}; + return $default if @_ == 2; + } + ); + + $code->(); + + $override_guard1->restore(); + $override_guard2->restore(); + mySociety::MaPit::configure() if $config->{MAPIT_URL};; +} + =head2 dbic_connect_info $connect_info = FixMyStreet->dbic_connect_info(); diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm index fda9d665c..1664f0f30 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -2,7 +2,11 @@ package FixMyStreet::App; use Moose; use namespace::autoclean; +# Should move away from Email::Send, but until then: +$Return::Value::NO_CLUCK = 1; + use Catalyst::Runtime 5.80; +use DateTime; use FixMyStreet; use FixMyStreet::Cobrand; use Memcached; @@ -23,6 +27,7 @@ use Catalyst ( 'Session::State::Cookie', # FIXME - we're using our own override atm 'Authentication', 'SmartURI', + 'Compress::Gzip', ); extends 'Catalyst'; @@ -45,7 +50,7 @@ __PACKAGE__->config( default_view => 'Web', # Serve anything in web dir that is not a .cgi script - static => { # + 'Plugin::Static::Simple' => { include_path => [ FixMyStreet->path_to("web") . "" ], ignore_extensions => ['cgi'], }, @@ -85,6 +90,13 @@ __PACKAGE__->config( # Start the application __PACKAGE__->setup(); +# Due to some current issues with proxyings, need to manually +# tell the code we're secure if we are. +after 'prepare_headers' => sub { + my $self = shift; + $self->req->secure( 1 ) if $self->config->{BASE_URL} eq 'https://www.zueriwieneu.ch'; +}; + # set up DB handle for old code FixMyStreet->configure_mysociety_dbhandle; @@ -188,6 +200,22 @@ 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(); + DateTime->DefaultLocale( 'de_CH' ); + } else { + DateTime->DefaultLocale( 'en_US' ); + } + + if (FixMyStreet->test_mode) { + # Is there a better way of altering $c->config that may have + # override_config involved? + $c->setup_finished(0); + $c->config( %{ FixMyStreet->config() } ); + $c->setup_finished(1); + } + return $c; } @@ -274,9 +302,8 @@ sub send_email { my $template = shift; my $extra_stash_values = shift || {}; - my $sender = $c->cobrand->contact_email; + my $sender = $c->config->{DO_NOT_REPLY_EMAIL}; my $sender_name = $c->cobrand->contact_name; - $sender =~ s/team/fms-DO-NOT-REPLY/; # create the vars to pass to the email template my $vars = { @@ -297,6 +324,8 @@ sub send_email { $email->header_set( ucfirst($_), $vars->{$_} ) for grep { $vars->{$_} } qw( to from subject); + return if $c->is_abuser( $email->header('To') ); + $email->header_set( 'Message-ID', sprintf('<fms-%s-%s@%s>', time(), unpack('h*', random_bytes(5, 1)), $c->config->{EMAIL_DOMAIN} ) ); @@ -307,6 +336,7 @@ sub send_email { { _template_ => $email->body, # will get line wrapped _parameters_ => {}, + _line_indent => '', $email->header_pairs } ) }; @@ -320,8 +350,10 @@ sub send_email { sub send_email_cron { my ( $c, $params, $env_from, $env_to, $nomail ) = @_; - $params->{'Message-ID'} = sprintf('<fms-cron-%s-%s@mysociety.org>', time(), - unpack('h*', random_bytes(5, 1)) + return 1 if $c->is_abuser( $env_to ); + + $params->{'Message-ID'} = sprintf('<fms-cron-%s-%s@%s>', time(), + unpack('h*', random_bytes(5, 1)), FixMyStreet->config('EMAIL_DOMAIN') ); $params->{_parameters_}->{signature} = ''; @@ -334,6 +366,7 @@ sub send_email_cron { # } #); + $params->{_line_indent} = ''; my $email = mySociety::Locale::in_gb_locale { mySociety::Email::construct_email($params) }; if ( FixMyStreet->test_mode ) { @@ -435,22 +468,39 @@ Hashref contains height, width and url keys. sub get_photo_params { my ($self, $key) = @_; - $key = ($key eq 'id') ? '' : "/$key"; return {} unless $self->photo; + $key = ($key eq 'id') ? '' : "/$key"; + + my $pre = "/photo$key/" . $self->id; + my $post = '.jpeg'; my $photo = {}; + if (length($self->photo) == 40) { - $photo->{url_full} = '/photo' . $key . '/' . $self->id . '.full.jpeg'; + $post .= '?' . $self->photo; + $photo->{url_full} = "$pre.full$post"; + # XXX Can't use size here because {url} (currently 250px height) may be + # being used, but at this point it doesn't yet exist to find the width + # $str = FixMyStreet->config('UPLOAD_DIR') . $self->photo . '.jpeg'; } else { - ( $photo->{width}, $photo->{height} ) = - Image::Size::imgsize( \$self->photo ); + my $str = \$self->photo; + ( $photo->{width}, $photo->{height} ) = Image::Size::imgsize( $str ); } - $photo->{url} = '/photo' . $key . '/' . $self->id . '.jpeg'; + + $photo->{url} = "$pre$post"; + $photo->{url_tn} = "$pre.tn$post"; + $photo->{url_fp} = "$pre.fp$post"; return $photo; } +sub is_abuser { + my ($c, $email) = @_; + my ($domain) = $email =~ m{ @ (.*) \z }x; + return $c->model('DB::Abuse')->search( { email => [ $email, $domain ] } )->first; +} + =head1 SEE ALSO L<FixMyStreet::App::Controller::Root>, L<Catalyst> diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index 7a2790b31..cfe165f43 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -4,10 +4,13 @@ use namespace::autoclean; BEGIN { extends 'Catalyst::Controller'; } +use Path::Class; use POSIX qw(strftime strcoll); -use Digest::MD5 qw(md5_hex); +use Digest::SHA qw(sha1_hex); use mySociety::EmailUtil qw(is_valid_email); use if !$ENV{TRAVIS}, 'Image::Magick'; +use DateTime::Format::Strptime; + use FixMyStreet::SendReport; @@ -23,23 +26,32 @@ Admin pages =cut -=head2 summary - -Redirect to index page. There to make the allowed pages stuff neater - -=cut - sub begin : Private { my ( $self, $c ) = @_; $c->uri_disposition('relative'); - if ( $c->cobrand->moniker eq 'seesomething' ) { + if ( $c->cobrand->moniker eq 'zurich' || $c->cobrand->moniker eq 'seesomething' ) { $c->detach( '/auth/redirect' ) unless $c->user_exists; - $c->detach( '/auth/redirect' ) unless $c->user->from_council; + $c->detach( '/auth/redirect' ) unless $c->user->from_body; } + if ( $c->cobrand->moniker eq 'zurich' ) { + $c->cobrand->admin_type(); + } +} + +sub auto : Private { + my ( $self, $c ) = @_; + + $c->forward('check_page_allowed'); } +=head2 summary + +Redirect to index page. There to make the allowed pages stuff neater + +=cut + sub summary : Path( 'summary' ) : Args(0) { my ( $self, $c ) = @_; $c->go( 'index' ); @@ -54,7 +66,9 @@ Displays some summary information for the requests. sub index : Path : Args(0) { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); + if ($c->cobrand->moniker eq 'zurich' && $c->stash->{admin_type} ne 'super') { + return $c->cobrand->admin(); + } my $site_restriction = $c->cobrand->site_restriction(); @@ -65,11 +79,9 @@ sub index : Path : Args(0) { %prob_counts = map { $_ => $prob_counts{$_} || 0 } - ('confirmed', 'investigating', 'in progress', 'closed', 'fixed - council', - 'fixed - user', 'fixed', 'unconfirmed', 'hidden', - 'partial', 'planned'); + ( FixMyStreet::DB::Result::Problem->all_states() ); $c->stash->{problems} = \%prob_counts; - $c->stash->{total_problems_live} += $prob_counts{$_} + $c->stash->{total_problems_live} += $prob_counts{$_} ? $prob_counts{$_} : 0 for ( FixMyStreet::DB::Result::Problem->visible_states() ); $c->stash->{total_problems_users} = $c->cobrand->problems->unique_users; @@ -120,13 +132,23 @@ sub index : Path : Args(0) { $c->stash->{categories} = $c->cobrand->problems->categories_summary(); + $c->stash->{total_bodies} = $c->model('DB::Body')->count(); + return 1; } -sub timeline : Path( 'timeline' ) : Args(0) { +sub config_page : Path( 'config' ) : Args(0) { my ($self, $c) = @_; + my $dir = $c->stash->{additional_template_paths}->[0]; + my $git_version = `cd $dir && git describe --tags`; + chomp $git_version; + $c->stash( + git_version => $git_version, + ); +} - $c->forward('check_page_allowed'); +sub timeline : Path( 'timeline' ) : Args(0) { + my ($self, $c) = @_; my $site_restriction = $c->cobrand->site_restriction(); my %time; @@ -137,34 +159,34 @@ sub timeline : Path( 'timeline' ) : Args(0) { my $probs = $c->cobrand->problems->timeline; foreach ($probs->all) { - push @{$time{$_->created->epoch}}, { type => 'problemCreated', date => $_->created_local, obj => $_ }; - push @{$time{$_->confirmed->epoch}}, { type => 'problemConfirmed', date => $_->confirmed_local, obj => $_ } if $_->confirmed; - push @{$time{$_->whensent->epoch}}, { type => 'problemSent', date => $_->whensent_local, obj => $_ } if $_->whensent; + push @{$time{$_->created->epoch}}, { type => 'problemCreated', date => $_->created, obj => $_ }; + push @{$time{$_->confirmed->epoch}}, { type => 'problemConfirmed', date => $_->confirmed, obj => $_ } if $_->confirmed; + push @{$time{$_->whensent->epoch}}, { type => 'problemSent', date => $_->whensent, obj => $_ } if $_->whensent; } my $questionnaires = $c->model('DB::Questionnaire')->timeline( $c->cobrand->restriction ); foreach ($questionnaires->all) { - push @{$time{$_->whensent->epoch}}, { type => 'quesSent', date => $_->whensent_local, obj => $_ }; - push @{$time{$_->whenanswered->epoch}}, { type => 'quesAnswered', date => $_->whenanswered_local, obj => $_ } if $_->whenanswered; + push @{$time{$_->whensent->epoch}}, { type => 'quesSent', date => $_->whensent, obj => $_ }; + push @{$time{$_->whenanswered->epoch}}, { type => 'quesAnswered', date => $_->whenanswered, obj => $_ } if $_->whenanswered; } my $updates = $c->model('DB::Comment')->timeline( $site_restriction ); foreach ($updates->all) { - push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created_local, obj => $_} ; + push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created, obj => $_} ; } my $alerts = $c->model('DB::Alert')->timeline_created( $c->cobrand->restriction ); foreach ($alerts->all) { - push @{$time{$_->whensubscribed->epoch}}, { type => 'alertSub', date => $_->whensubscribed_local, obj => $_ }; + push @{$time{$_->whensubscribed->epoch}}, { type => 'alertSub', date => $_->whensubscribed, obj => $_ }; } $alerts = $c->model('DB::Alert')->timeline_disabled( $c->cobrand->restriction ); foreach ($alerts->all) { - push @{$time{$_->whendisabled->epoch}}, { type => 'alertDel', date => $_->whendisabled_local, obj => $_ }; + push @{$time{$_->whendisabled->epoch}}, { type => 'alertDel', date => $_->whendisabled, obj => $_ }; } $c->model('DB')->schema->storage->sql_maker->quote_char( '' ); @@ -177,8 +199,6 @@ sub timeline : Path( 'timeline' ) : Args(0) { sub questionnaire : Path('questionnaire') : Args(0) { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); - my $questionnaires = $c->model('DB::Questionnaire')->search( { whenanswered => { '!=', undef } }, { group_by => [ 'ever_reported' ], @@ -209,10 +229,10 @@ sub questionnaire : Path('questionnaire') : Args(0) { return 1; } -sub council_list : Path('council_list') : Args(0) { +sub bodies : Path('bodies') : Args(0) { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); + $c->forward( 'get_token' ); my $edit_activity = $c->model('DB::ContactsHistory')->search( undef, @@ -226,54 +246,78 @@ sub council_list : Path('council_list') : Args(0) { $c->stash->{edit_activity} = $edit_activity; - # Not London, as treated separately - my $area_types = $c->cobrand->moniker eq 'emptyhomes' - ? $c->cobrand->area_types - : [ grep { $_ ne 'LBO' } @{ $c->cobrand->area_types } ]; - my $areas = mySociety::MaPit::call('areas', $area_types); + my $posted = $c->req->param('posted') || ''; + if ( $posted eq 'body' ) { + $c->forward('check_for_super_user'); + $c->forward('check_token'); + + my $params = $c->forward('body_params'); + my $body = $c->model('DB::Body')->create( $params ); + my $area_ids = $c->req->params->{area_ids}; + if ($area_ids) { + $area_ids = [ $area_ids ] unless ref $area_ids; + foreach (@$area_ids) { + $c->model('DB::BodyArea')->create( { body => $body, area_id => $_ } ); + } + } + + $c->stash->{updated} = _('New body added'); + } - my @councils_ids = sort { strcoll($areas->{$a}->{name}, $areas->{$b}->{name}) } keys %$areas; - @councils_ids = $c->cobrand->filter_all_council_ids_list( @councils_ids ); + $c->forward( 'fetch_all_bodies' ); + + # XXX For fixmystreet.com, need to exclude bodies that are covering London. + # But soon, this means just don't have bodies covering London. my $contacts = $c->model('DB::Contact')->search( undef, { - select => [ 'area_id', { count => 'id' }, { count => \'case when deleted then 1 else null end' }, + select => [ 'body_id', { count => 'id' }, { count => \'case when deleted then 1 else null end' }, { count => \'case when confirmed then 1 else null end' } ], - as => [qw/area_id c deleted confirmed/], - group_by => [ 'area_id' ], + as => [qw/body_id c deleted confirmed/], + group_by => [ 'body_id' ], result_class => 'DBIx::Class::ResultClass::HashRefInflator' } ); - my %council_info = map { $_->{area_id} => $_ } $contacts->all; - - my @no_info = grep { !$council_info{$_} } @councils_ids; - my @one_plus_deleted = grep { $council_info{$_} && $council_info{$_}->{deleted} } @councils_ids; - my @unconfirmeds = grep { $council_info{$_} && !$council_info{$_}->{deleted} && $council_info{$_}->{confirmed} != $council_info{$_}->{c} } @councils_ids; - my @all_confirmed = grep { $council_info{$_} && !$council_info{$_}->{deleted} && $council_info{$_}->{confirmed} == $council_info{$_}->{c} } @councils_ids; + my %council_info = map { $_->{body_id} => $_ } $contacts->all; - $c->stash->{areas} = $areas; $c->stash->{counts} = \%council_info; - $c->stash->{no_info} = \@no_info; - $c->stash->{one_plus_deleted} = \@one_plus_deleted; - $c->stash->{unconfirmeds} = \@unconfirmeds; - $c->stash->{all_confirmed} = \@all_confirmed; + + $c->forward( 'body_form_dropdowns' ); return 1; } -sub council_contacts : Path('council_contacts') : Args(1) { - my ( $self, $c, $area_id ) = @_; +sub body_form_dropdowns : Private { + my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); + my $areas; + my $whitelist = $c->config->{MAPIT_ID_WHITELIST}; - my $posted = $c->req->param('posted') || ''; - $c->stash->{area_id} = $area_id; + if ( $whitelist && ref $whitelist eq 'ARRAY' && @$whitelist ) { + $areas = mySociety::MaPit::call('areas', $whitelist); + } else { + $areas = mySociety::MaPit::call('areas', $c->cobrand->area_types); + } + $c->stash->{areas} = [ sort { strcoll($a->{name}, $b->{name}) } values %$areas ]; + + my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } keys %{ FixMyStreet::SendReport->get_senders }; + $c->stash->{send_methods} = \@methods; +} +sub body : Path('body') : Args(1) { + my ( $self, $c, $body_id ) = @_; + + $c->stash->{body_id} = $body_id; + + $c->forward( 'check_for_super_user' ); $c->forward( 'get_token' ); + $c->forward( 'lookup_body' ); + $c->forward( 'fetch_all_bodies' ); + $c->forward( 'body_form_dropdowns' ); - if ( $posted ) { + if ( $c->req->param('posted') ) { $c->log->debug( 'posted' ); $c->forward('update_contacts'); } @@ -283,11 +327,18 @@ sub council_contacts : Path('council_contacts') : Args(1) { return 1; } +sub check_for_super_user : Private { + my ( $self, $c ) = @_; + if ( $c->cobrand->moniker eq 'zurich' && $c->stash->{admin_type} ne 'super' ) { + $c->detach('/page_error_404_not_found', []); + } +} + sub update_contacts : Private { my ( $self, $c ) = @_; my $posted = $c->req->param('posted'); - my $editor = $c->req->remote_user || _('*unknown*'); + my $editor = $c->forward('get_user'); if ( $posted eq 'new' ) { $c->forward('check_token'); @@ -299,7 +350,7 @@ sub update_contacts : Private { my $contact = $c->model('DB::Contact')->find_or_new( { - area_id => $c->stash->{area_id}, + body_id => $c->stash->{body_id}, category => $category, } ); @@ -333,7 +384,7 @@ sub update_contacts : Private { my $contacts = $c->model('DB::Contact')->search( { - area_id => $c->stash->{area_id}, + body_id => $c->stash->{body_id}, category => { -in => \@categories }, } ); @@ -348,67 +399,53 @@ sub update_contacts : Private { ); $c->stash->{updated} = _('Values updated'); - } elsif ( $posted eq 'open311' ) { + } elsif ( $posted eq 'body' ) { + $c->forward('check_for_super_user'); $c->forward('check_token'); - my %params = map { $_ => $c->req->param($_) || '' } qw/open311_id endpoint jurisdiction api_key area_id send_method send_comments suppress_alerts comment_user_id devolved/; - - if ( $params{open311_id} ) { - my $conf = $c->model('DB::Open311Conf')->find( { id => $params{open311_id} } ); - - $conf->endpoint( $params{endpoint} ); - $conf->jurisdiction( $params{jurisdiction} ); - $conf->api_key( $params{api_key} ); - $conf->send_method( $params{send_method} ); - $conf->send_comments( $params{send_comments} || 0); - $conf->suppress_alerts( $params{suppress_alerts} || 0); - $conf->comment_user_id( $params{comment_user_id} || undef ); - $conf->can_be_devolved( $params{devolved} || 0 ); - - $conf->update(); - - $c->stash->{updated} = _('Configuration updated'); - } else { - my $conf = $c->model('DB::Open311Conf')->find_or_new( { area_id => $params{area_id} } ); - - $conf->endpoint( $params{endpoint} ); - $conf->jurisdiction( $params{jurisdiction} ); - $conf->api_key( $params{api_key} ); - $conf->send_method( $params{send_method} ); - $conf->send_comments( $params{send_comments} || 0); - $conf->suppress_alerts( $params{suppress_alerts} || 0); - $conf->comment_user_id( $params{comment_user_id} || undef ); - $conf->can_be_devolved( $params{devolved} || 0 ); - - $conf->insert(); - - $c->stash->{updated} = _('Configuration updated - contacts will be generated automatically later'); + my $params = $c->forward( 'body_params' ); + $c->stash->{body}->update( $params ); + my @current = $c->stash->{body}->body_areas->all; + my %current = map { $_->area_id => 1 } @current; + my $area_ids = $c->req->params->{area_ids}; + if ($area_ids) { + $area_ids = [ $area_ids ] unless ref $area_ids; + foreach (@$area_ids) { + $c->model('DB::BodyArea')->find_or_create( { body => $c->stash->{body}, area_id => $_ } ); + delete $current{$_}; + } } + # Remove any others + $c->stash->{body}->body_areas->search( { area_id => [ keys %current ] } )->delete; + + $c->stash->{updated} = _('Configuration updated - contacts will be generated automatically later'); } } -sub display_contacts : Private { +sub body_params : Private { my ( $self, $c ) = @_; - $c->forward('setup_council_details'); - - my $area_id = $c->stash->{area_id}; - - my $contacts = $c->model('DB::Contact')->search( - { area_id => $area_id }, - { order_by => ['category'] } + my @fields = qw/name endpoint jurisdiction api_key send_method send_comments suppress_alerts send_extended_statuses comment_user_id can_be_devolved parent deleted/; + my %defaults = map { $_ => '' } @fields; + %defaults = ( %defaults, + send_comments => 0, + suppress_alerts => 0, + comment_user_id => undef, + send_extended_statuses => 0, + can_be_devolved => 0, + parent => undef, + deleted => 0, ); + my %params = map { $_ => $c->req->param($_) || $defaults{$_} } @fields; + return \%params; +} - $c->stash->{contacts} = $contacts; - - my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } keys %{ FixMyStreet::SendReport->get_senders }; - $c->stash->{send_methods} = \@methods; - - my $open311 = $c->model('DB::Open311Conf')->search( - { area_id => $area_id } - ); +sub display_contacts : Private { + my ( $self, $c ) = @_; - $c->stash->{open311} = $open311; + my $contacts = $c->stash->{body}->contacts->search(undef, { order_by => [ 'category' ] } ); + $c->stash->{contacts} = $contacts; + $c->stash->{live_contacts} = $contacts->search({ deleted => 0 }); if ( $c->req->param('text') && $c->req->param('text') == 1 ) { $c->stash->{template} = 'admin/council_contacts.txt'; @@ -419,59 +456,52 @@ sub display_contacts : Private { return 1; } -sub setup_council_details : Private { +sub lookup_body : Private { my ( $self, $c ) = @_; - my $area_id = $c->stash->{area_id}; - - my $mapit_data = mySociety::MaPit::call('area', $area_id); - - $c->stash->{council_name} = $mapit_data->{name}; - - my $example_postcode = mySociety::MaPit::call('area/example_postcode', $area_id); - - if ($example_postcode && ! ref $example_postcode) { - $c->stash->{example_pc} = $example_postcode; + my $body_id = $c->stash->{body_id}; + my $body = $c->model('DB::Body')->find($body_id); + $c->detach( '/page_error_404_not_found' ) + unless $body; + $c->stash->{body} = $body; + + if ($body->body_areas->first) { + my $example_postcode = mySociety::MaPit::call('area/example_postcode', $body->body_areas->first->area_id); + if ($example_postcode && ! ref $example_postcode) { + $c->stash->{example_pc} = $example_postcode; + } } return 1; } -sub council_edit_all : Path('council_edit') { - my ( $self, $c, $area_id, @category ) = @_; +# This is for if the category name contains a '/' +sub body_edit_all : Path('body_edit') { + my ( $self, $c, $body_id, @category ) = @_; my $category = join( '/', @category ); - $c->go( 'council_edit', [ $area_id, $category ] ); + $c->go( 'body_edit', [ $body_id, $category ] ); } -sub council_edit : Path('council_edit') : Args(2) { - my ( $self, $c, $area_id, $category ) = @_; +sub body_edit : Path('body_edit') : Args(2) { + my ( $self, $c, $body_id, $category ) = @_; - $c->forward('check_page_allowed'); - - $c->stash->{area_id} = $area_id; + $c->stash->{body_id} = $body_id; $c->forward( 'get_token' ); - $c->forward('setup_council_details'); - - my $contact = $c->model('DB::Contact')->search( - { - area_id => $area_id, - category => $category - } - )->first; + $c->forward( 'lookup_body' ); + my $contact = $c->stash->{body}->contacts->search( { category => $category } )->first; $c->stash->{contact} = $contact; my $history = $c->model('DB::ContactsHistory')->search( { - area_id => $area_id, + body_id => $body_id, category => $category }, { order_by => ['contacts_history_id'] }, ); - $c->stash->{history} = $history; my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } keys %{ FixMyStreet::SendReport->get_senders }; @@ -480,13 +510,30 @@ sub council_edit : Path('council_edit') : Args(2) { return 1; } -sub search_reports : Path('search_reports') { +sub reports : Path('reports') { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); + my $query = {}; + if ( $c->cobrand->moniker eq 'zurich' ) { + my $type = $c->stash->{admin_type}; + my $body = $c->stash->{body}; + if ( $type eq 'dm' ) { + my @children = map { $_->id } $body->bodies->all; + my @all = (@children, $body->id); + $query = { bodies_str => \@all }; + } elsif ( $type eq 'sdm' ) { + $query = { bodies_str => $body->id }; + } + } + + my $order = $c->req->params->{o} || 'created'; + my $dir = defined $c->req->params->{d} ? $c->req->params->{d} : 1; + $c->stash->{order} = $order; + $c->stash->{dir} = $dir; + $order .= ' desc' if $dir; if (my $search = $c->req->param('search')) { - $c->stash->{searched} = 1; + $c->stash->{searched} = $search; my $site_restriction = $c->cobrand->site_restriction; @@ -503,37 +550,40 @@ sub search_reports : Path('search_reports') { $c->model('DB')->schema->storage->sql_maker->quote_char( '"' ); $c->model('DB')->schema->storage->sql_maker->name_sep( '.' ); - my $query; if (is_valid_email($search)) { - $query = [ + $query->{'-or'} = [ 'user.email' => { ilike => $like_search }, ]; } elsif ($search =~ /^id:(\d+)$/) { - $query = [ + $query->{'-or'} = [ 'me.id' => int($1), ]; } elsif ($search =~ /^area:(\d+)$/) { - $query = [ + $query->{'-or'} = [ 'me.areas' => { like => "%,$1,%" } ]; + } elsif ($search =~ /^ref:(\d+)$/) { + $query->{'-or'} = [ + 'me.external_id' => { like => "%$1%" } + ]; } else { - $query = [ + $query->{'-or'} = [ 'me.id' => $search_n, 'user.email' => { ilike => $like_search }, + 'me.external_id' => { ilike => $like_search }, 'me.name' => { ilike => $like_search }, 'me.title' => { ilike => $like_search }, detail => { ilike => $like_search }, - council => { like => $like_search }, + bodies_str => { like => $like_search }, cobrand_data => { like => $like_search }, ]; } + my $problems = $c->cobrand->problems->search( - { - -or => $query, - }, + $query, { prefetch => 'user', - order_by => [\"(state='hidden')",'created'] + order_by => [ \"(state='hidden')", \$order ] } ); @@ -542,9 +592,6 @@ sub search_reports : Path('search_reports') { # will have been turned off $c->stash->{problems} = [ $problems->all ]; - $c->stash->{edit_council_contacts} = 1 - if ( grep {$_ eq 'councilcontacts'} keys %{$c->stash->{allowed_pages}}); - if (is_valid_email($search)) { $query = [ 'user.email' => { ilike => $like_search }, @@ -574,9 +621,9 @@ sub search_reports : Path('search_reports') { -or => $query, }, { - -select => [ 'me.*', qw/problem.council problem.state/ ], + -select => [ 'me.*', qw/problem.bodies_str problem.state/ ], prefetch => [qw/user problem/], - order_by => [\"(me.state='hidden')",\"(problem.state='hidden')",'me.created'] + order_by => [ \"(me.state='hidden')", \"(problem.state='hidden')", 'me.created' ] } ); $c->stash->{updates} = [ $updates->all ]; @@ -584,7 +631,20 @@ sub search_reports : Path('search_reports') { # Switch quoting back off. See above for explanation of this. $c->model('DB')->schema->storage->sql_maker->quote_char( '' ); + } else { + + my $page = $c->req->params->{p} || 1; + my $problems = $c->cobrand->problems->search( + $query, + { order_by => $order } + )->page( $page ); + $c->stash->{problems} = [ $problems->all ]; + $c->stash->{pager} = $problems->pager; } + + $c->stash->{edit_body_contacts} = 1 + if ( grep {$_ eq 'body'} keys %{$c->stash->{allowed_pages}}); + } sub report_edit : Path('report_edit') : Args(1) { @@ -592,11 +652,7 @@ sub report_edit : Path('report_edit') : Args(1) { my $site_restriction = $c->cobrand->site_restriction; - my $problem = $c->cobrand->problems->search( - { - id => $id, - } - )->first; + my $problem = $c->cobrand->problems->search( { id => $id } )->first; $c->detach( '/page_error_404_not_found' ) unless $problem; @@ -604,7 +660,34 @@ sub report_edit : Path('report_edit') : Args(1) { $c->stash->{problem} = $problem; $c->forward('get_token'); - $c->forward('check_page_allowed'); + + if ( $c->cobrand->moniker eq 'zurich' ) { + $c->stash->{page} = 'admin'; + FixMyStreet::Map::display_map( + $c, + latitude => $problem->latitude, + longitude => $problem->longitude, + pins => $problem->used_map + ? [ { + latitude => $problem->latitude, + longitude => $problem->longitude, + colour => $c->cobrand->pin_colour($problem), + type => 'big', + } ] + : [], + ); + } + + if ( $c->req->param('rotate_photo') ) { + $c->forward('rotate_photo'); + return 1; + } + + if ( $c->cobrand->moniker eq 'zurich' ) { + my $done = $c->cobrand->admin_report_edit(); + return if $done; + } + $c->forward('check_email_for_abuse', [ $problem->user->email ] ); $c->stash->{updates} = @@ -633,9 +716,6 @@ sub report_edit : Path('report_edit') : Args(1) { elsif ( $c->req->param('banuser') ) { $c->forward('ban_user'); } - elsif ( $c->req->param('rotate_photo') ) { - $c->forward('rotate_photo'); - } elsif ( $c->req->param('submit') ) { $c->forward('check_token'); @@ -664,6 +744,7 @@ sub report_edit : Path('report_edit') : Args(1) { || $c->req->param('email') ne $problem->user->email || $c->req->param('title') ne $problem->title || $c->req->param('detail') ne $problem->detail + || ($c->req->param('body') && $c->req->param('body') ne $problem->bodies_str) || $flagged != $problem->flagged || $non_public != $problem->non_public ) { @@ -673,8 +754,10 @@ sub report_edit : Path('report_edit') : Args(1) { $problem->anonymous( $c->req->param('anonymous') ); $problem->title( $c->req->param('title') ); $problem->detail( $c->req->param('detail') ); - $problem->state( $c->req->param('state') ); + $problem->state( $new_state ); $problem->name( $c->req->param('name') ); + $problem->bodies_str( $c->req->param('body') ) if $c->req->param('body'); + $problem->flagged( $flagged ); $problem->non_public( $non_public ); @@ -687,11 +770,16 @@ sub report_edit : Path('report_edit') : Args(1) { $problem->user( $user ); } + # Deal with photos if ( $c->req->param('remove_photo') ) { $problem->photo(undef); } - if ( $new_state eq 'confirmed' and $old_state eq 'unconfirmed' ) { + if ( $c->req->param('remove_photo') || $new_state eq 'hidden' ) { + unlink glob FixMyStreet->path_to( 'web', 'photo', $problem->id . '.*' ); + } + + if ( $problem->is_visible() and $old_state eq 'unconfirmed' ) { $problem->confirmed( \'ms_current_timestamp()' ); } @@ -721,26 +809,22 @@ sub report_edit : Path('report_edit') : Args(1) { return 1; } -sub search_users: Path('search_users') : Args(0) { +sub users: Path('users') : Args(0) { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); - if (my $search = $c->req->param('search')) { - $c->stash->{searched} = 1; + $c->stash->{searched} = $search; - my $search = $c->req->param('search'); my $isearch = '%' . $search . '%'; - my $search_n = 0; $search_n = int($search) if $search =~ /^\d+$/; my $users = $c->model('DB::User')->search( { -or => [ - email => { ilike => $isearch }, - name => { ilike => $isearch }, - from_council => $search_n, + email => { ilike => $isearch }, + name => { ilike => $isearch }, + from_body => $search_n, ] } ); @@ -762,6 +846,19 @@ sub search_users: Path('search_users') : Args(0) { } } + } else { + $c->forward('get_token'); + $c->forward('fetch_all_bodies'); + + # Admin users by default + my $users = $c->model('DB::User')->search( + { from_body => { '!=', undef } }, + { order_by => 'name' } + ); + my @users = $users->all; + my %email2user = map { $_->email => $_ } @users; + $c->stash->{users} = \@users; + } return 1; @@ -782,7 +879,6 @@ sub update_edit : Path('update_edit') : Args(1) { unless $update; $c->forward('get_token'); - $c->forward('check_page_allowed'); $c->stash->{update} = $update; @@ -821,10 +917,14 @@ sub update_edit : Path('update_edit') : Args(1) { $update->photo(undef); } + if ( $c->req->param('remove_photo') || $new_state eq 'hidden' ) { + unlink glob FixMyStreet->path_to( 'web', 'photo', 'c', $update->id . '.*' ); + } + $update->name( $c->req->param('name') || '' ); $update->text( $c->req->param('text') ); $update->anonymous( $c->req->param('anonymous') ); - $update->state( $c->req->param('state') ); + $update->state( $new_state ); if ( $c->req->param('email') ne $update->user->email ) { my $user = @@ -837,6 +937,11 @@ sub update_edit : Path('update_edit') : Args(1) { if ( $new_state eq 'confirmed' and $old_state eq 'unconfirmed' ) { $update->confirmed( \'ms_current_timestamp()' ); + if ( $update->problem_state && $update->created > $update->problem->lastupdate ) { + $update->problem->state( $update->problem_state ); + $update->problem->lastupdate( \'ms_current_timestamp()' ); + $update->problem->update; + } } $update->update; @@ -868,16 +973,51 @@ sub update_edit : Path('update_edit') : Args(1) { return 1; } +sub user_add : Path('user_edit') : Args(0) { + my ( $self, $c ) = @_; + + $c->stash->{template} = 'admin/user_edit.html'; + $c->forward('get_token'); + $c->forward('fetch_all_bodies'); + + return 1 unless $c->req->param('submit'); + + $c->forward('check_token'); + + if ( $c->cobrand->moniker eq 'zurich' and $c->req->param('email') eq '' ) { + $c->stash->{field_errors}->{email} = _('Please enter a valid email'); + return 1; + } + + return unless $c->req->param('name') && $c->req->param('email'); + + my $user = $c->model('DB::User')->find_or_create( { + name => $c->req->param('name'), + email => $c->req->param('email'), + from_body => $c->req->param('body') || undef, + flagged => $c->req->param('flagged') || 0, + }, { + key => 'users_email_key' + } ); + $c->stash->{user} = $user; + + $c->forward( 'log_edit', [ $user->id, 'user', 'edit' ] ); + + $c->stash->{status_message} = + '<p><em>' . _('Updated!') . '</em></p>'; + + return 1; +} + sub user_edit : Path('user_edit') : Args(1) { my ( $self, $c, $id ) = @_; - $c->forward('check_page_allowed'); $c->forward('get_token'); my $user = $c->model('DB::User')->find( { id => $id } ); $c->stash->{user} = $user; - $c->forward('set_up_council_details'); + $c->forward('fetch_all_bodies'); if ( $c->req->param('submit') ) { $c->forward('check_token'); @@ -886,14 +1026,21 @@ sub user_edit : Path('user_edit') : Args(1) { if ( $user->email ne $c->req->param('email') || $user->name ne $c->req->param('name' ) || - $user->from_council != $c->req->param('council') ) { + ($user->from_body && $user->from_body->id ne $c->req->param('body')) || + (!$user->from_body && $c->req->param('body')) + ) { $edited = 1; } $user->name( $c->req->param('name') ); $user->email( $c->req->param('email') ); - $user->from_council( $c->req->param('council') || undef ); + $user->from_body( $c->req->param('body') || undef ); $user->flagged( $c->req->param('flagged') || 0 ); + + if ( $c->cobrand->moniker eq 'zurich' and $user->email eq '' ) { + $c->stash->{field_errors}->{email} = _('Please enter a valid email'); + return 1; + } $user->update; if ($edited) { @@ -907,11 +1054,9 @@ sub user_edit : Path('user_edit') : Args(1) { return 1; } -sub list_flagged : Path('list_flagged') : Args(0) { +sub flagged : Path('flagged') : Args(0) { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); - my $problems = $c->model('DB::Problem')->search( { flagged => 1 } ); # pass in as array ref as using same template as search_reports @@ -919,8 +1064,20 @@ sub list_flagged : Path('list_flagged') : Args(0) { $c->stash->{problems} = [ $problems->all ]; my $users = $c->model('DB::User')->search( { flagged => 1 } ); + my @users = $users->all; + my %email2user = map { $_->email => $_ } @users; + $c->stash->{users} = [ @users ]; - $c->stash->{users} = $users; + my @abuser_emails = $c->model('DB::Abuse')->all(); + + foreach my $email (@abuser_emails) { + # Slight abuse of the boolean flagged value + if ($email2user{$email->email}) { + $email2user{$email->email}->flagged( 2 ); + } else { + push @{$c->stash->{users}}, { email => $email->email, flagged => 2 }; + } + } return 1; } @@ -928,37 +1085,24 @@ sub list_flagged : Path('list_flagged') : Args(0) { sub stats : Path('stats') : Args(0) { my ( $self, $c ) = @_; - $c->forward('check_page_allowed'); - - $c->forward('set_up_council_details'); + $c->forward('fetch_all_bodies'); - if ( $c->cobrand->moniker eq 'seesomething' ) { + if ( $c->cobrand->moniker eq 'seesomething' || $c->cobrand->moniker eq 'zurich' ) { return $c->cobrand->admin_stats(); } if ( $c->req->param('getcounts') ) { my ( $start_date, $end_date, @errors ); + my $parser = DateTime::Format::Strptime->new( pattern => '%d/%m/%Y' ); - eval { - $start_date = DateTime->new( - year => $c->req->param('start_date_year'), - month => $c->req->param('start_date_month'), - day => $c->req->param('start_date_day'), - ); - }; + $start_date = $parser-> parse_datetime ( $c->req->param('start_date') ); - push @errors, _('Invalid start date') if $@; + push @errors, _('Invalid start date') unless defined $start_date; - eval { - $end_date = DateTime->new( - year => $c->req->param('end_date_year'), - month => $c->req->param('end_date_month'), - day => $c->req->param('end_date_day'), - ); - }; + $end_date = $parser-> parse_datetime ( $c->req->param('end_date') ) ; - push @errors, _('Invalid end date') if $@; + push @errors, _('Invalid end date') unless defined $end_date; $c->stash->{errors} = \@errors; $c->stash->{start_date} = $start_date; @@ -970,11 +1114,11 @@ sub stats : Path('stats') : Args(0) { my $bymonth = $c->req->param('bymonth'); $c->stash->{bymonth} = $bymonth; - my ( %council, %dates ); - $council{council} = { like => $c->req->param('council') } - if $c->req->param('council'); + my ( %body, %dates ); + $body{bodies_str} = { like => $c->req->param('body') } + if $c->req->param('body'); - $c->stash->{selected_council} = $c->req->param('council'); + $c->stash->{selected_body} = $c->req->param('body'); my $field = 'confirmed'; @@ -1009,7 +1153,7 @@ sub stats : Path('stats') : Args(0) { $field => { '>=', $start_date}, $field => { '<=', $end_date + $one_day }, ], - %council, + %body, %dates, }, \%select, @@ -1038,16 +1182,17 @@ sub set_allowed_pages : Private { if( !$pages ) { $pages = { 'summary' => [_('Summary'), 0], - 'council_list' => [_('Bodies'), 1], - 'search_reports' => [_('Reports'), 2], + 'bodies' => [_('Bodies'), 1], + 'reports' => [_('Reports'), 2], 'timeline' => [_('Timeline'), 3], 'questionnaire' => [_('Survey'), 4], - 'search_users' => [_('Users'), 5], - 'list_flagged' => [_('Flagged'), 6], + 'users' => [_('Users'), 5], + 'flagged' => [_('Flagged'), 6], 'stats' => [_('Stats'), 6], + 'config' => [ undef, undef ], 'user_edit' => [undef, undef], - 'council_contacts' => [undef, undef], - 'council_edit' => [undef, undef], + 'body' => [undef, undef], + 'body_edit' => [undef, undef], 'report_edit' => [undef, undef], 'update_edit' => [undef, undef], 'abuse_edit' => [undef, undef], @@ -1062,6 +1207,16 @@ sub set_allowed_pages : Private { return 1; } +sub get_user : Private { + my ( $self, $c ) = @_; + + my $user = $c->req->remote_user(); + $user ||= ($c->user && $c->user->name); + $user ||= ''; + + return $user; +} + =item get_token Generate a token based on user and secret @@ -1072,12 +1227,8 @@ sub get_token : Private { my ( $self, $c ) = @_; my $secret = $c->model('DB::Secret')->search()->first; - - my $user = $c->req->remote_user(); - $user ||= ''; - - my $token = md5_hex(($user . $secret->secret)); - + my $user = $c->forward('get_user'); + my $token = sha1_hex($user . $secret->secret); $c->stash->{token} = $token; return 1; @@ -1104,7 +1255,7 @@ sub check_token : Private { $c->forward( 'log_edit', [ $object_id, $object_type, $action_performed ] ); -Adds an entry into the admin_log table using the current remote_user. +Adds an entry into the admin_log table using the current user. =cut @@ -1112,7 +1263,7 @@ sub log_edit : Private { my ( $self, $c, $id, $object_type, $action ) = @_; $c->model('DB::AdminLog')->create( { - admin_user => ( $c->req->remote_user() || '' ), + admin_user => $c->forward('get_user'), object_type => $object_type, action => $action, object_id => $id, @@ -1232,21 +1383,36 @@ sub rotate_photo : Private { my ( $self, $c ) =@_; my $direction = $c->req->param('rotate_photo'); + return unless $direction eq _('Rotate Left') or $direction eq _('Rotate Right'); - return unless $direction =~ /Left/ or $direction =~ /Right/; - - my $photo = _rotate_image( $c->stash->{problem}->photo, $direction =~ /Left/ ? -90 : 90 ); + my $photo = $c->stash->{problem}->photo; + my $file; - if ( $photo ) { - $c->stash->{rotated} = 1; - $c->stash->{problem}->photo( $photo ); - $c->stash->{problem}->update(); + # If photo field contains a hash + if ( length($photo) == 40 ) { + $file = file( $c->config->{UPLOAD_DIR}, "$photo.jpeg" ); + $photo = $file->slurp; } + $photo = _rotate_image( $photo, $direction eq _('Rotate Left') ? -90 : 90 ); + return unless $photo; + + # Write out to new location + my $fileid = sha1_hex($photo); + $file = file( $c->config->{UPLOAD_DIR}, "$fileid.jpeg" ); + + my $fh = $file->open('w'); + print $fh $photo; + close $fh; + + unlink glob FixMyStreet->path_to( 'web', 'photo', $c->stash->{problem}->id . '.*' ); + + $c->stash->{problem}->photo( $fileid ); + $c->stash->{problem}->update(); + return 1; } - =head2 check_page_allowed Checks if the current catalyst action is in the list of allowed pages and @@ -1270,16 +1436,16 @@ sub check_page_allowed : Private { return 1; } -sub set_up_council_details : Private { +sub fetch_all_bodies : Private { my ($self, $c ) = @_; - my $areas = mySociety::MaPit::call('areas', $c->cobrand->area_types); - - my @councils_ids = sort { strcoll($areas->{$a}->{name}, $areas->{$b}->{name}) } keys %$areas; - @councils_ids = $c->cobrand->filter_all_council_ids_list( @councils_ids ); - - $c->stash->{council_ids} = \@councils_ids; - $c->stash->{council_details} = $areas; + my @bodies = $c->model('DB::Body')->all; + if ( $c->cobrand->moniker eq 'zurich' ) { + @bodies = $c->cobrand->admin_fetch_all_bodies( @bodies ); + } else { + @bodies = sort { strcoll($a->name, $b->name) } @bodies; + } + $c->stash->{bodies} = \@bodies; return 1; } diff --git a/perllib/FixMyStreet/App/Controller/Alert.pm b/perllib/FixMyStreet/App/Controller/Alert.pm index 91ea61fbc..e821b7467 100644 --- a/perllib/FixMyStreet/App/Controller/Alert.pm +++ b/perllib/FixMyStreet/App/Controller/Alert.pm @@ -407,13 +407,13 @@ Generate the details required to display the council/ward/area RSS feeds sub setup_council_rss_feeds : Private { my ( $self, $c ) = @_; - $c->stash->{council_check_action} = 'alert'; - unless ( $c->forward('/council/load_and_check_councils_and_wards') ) { + $c->stash->{area_check_action} = 'alert'; + unless ( $c->forward('/council/load_and_check_areas_and_wards') ) { $c->go('index'); } ( $c->stash->{options}, $c->stash->{reported_to_options} ) = - $c->cobrand->council_rss_alert_options( $c->stash->{all_councils}, $c ); + $c->cobrand->council_rss_alert_options( $c->stash->{all_areas}, $c ); return 1; } diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm index f2bb23350..41e0ad947 100644 --- a/perllib/FixMyStreet/App/Controller/Around.pm +++ b/perllib/FixMyStreet/App/Controller/Around.pm @@ -40,11 +40,12 @@ sub around_index : Path : Args(0) { my $partial_report = $c->forward('load_partial'); # Try to create a location for whatever we have - return - unless $c->forward('/location/determine_location_from_coords') - || $c->forward('/location/determine_location_from_pc'); + my $ret = $c->forward('/location/determine_location_from_coords') + || $c->forward('/location/determine_location_from_pc'); + return unless $ret; + return $c->res->redirect('/') if $ret == -1 && !$partial_report; - # Check to see if the spot is covered by a council - if not show an error. + # Check to see if the spot is covered by a area - if not show an error. return unless $c->cobrand->moniker eq 'fixmybarangay' || $c->forward('check_location_is_acceptable'); # If we have a partial - redirect to /report/new so that it can be @@ -192,7 +193,7 @@ sub display_location : Private { longitude => $p->longitude, colour => $colour, id => $p->id, - title => $p->title, + title => $p->title_safe, } } @$on_map_all, @$around_map; } @@ -212,7 +213,7 @@ sub display_location : Private { =head2 check_location_is_acceptable -Find the lat and lon in stash and check that they are acceptable to the council, +Find the lat and lon in stash and check that they are acceptable to the area, and that they are in UK (if we are in UK). =cut @@ -220,10 +221,10 @@ and that they are in UK (if we are in UK). sub check_location_is_acceptable : Private { my ( $self, $c ) = @_; - # check that there are councils that can accept this location - $c->stash->{council_check_action} = 'submit_problem'; - $c->stash->{remove_redundant_councils} = 1; - return $c->forward('/council/load_and_check_councils'); + # check that there are areas that can accept this location + $c->stash->{area_check_action} = 'submit_problem'; + $c->stash->{remove_redundant_areas} = 1; + return $c->forward('/council/load_and_check_areas'); } =head2 /ajax @@ -281,6 +282,66 @@ sub ajax : Path('/ajax') { $c->res->body($body); } + +sub location_autocomplete : Path('/ajax/geocode') { + my ( $self, $c ) = @_; + $c->res->content_type('application/json; charset=utf-8'); + unless ( $c->req->param('term') ) { + $c->res->status(404); + $c->res->body(''); + return; + } + # we want the match even if there's no ambiguity, so recommendation doesn't + # disappear when it's the last choice being offered in the autocomplete. + $c->stash->{allow_single_geocode_match_strings} = 1; + return $self->_geocode( $c, $c->req->param('term') ); +} + +sub location_lookup : Path('/ajax/lookup_location') { + my ( $self, $c ) = @_; + $c->res->content_type('application/json; charset=utf-8'); + unless ( $c->req->param('term') ) { + $c->res->status(404); + $c->res->body(''); + return; + } + + return $self->_geocode( $c, $c->req->param('term') ); +} + +sub _geocode : Private { + my ( $self, $c, $term ) = @_; + + my ( $lat, $long, $suggestions ) = + FixMyStreet::Geocode::lookup( $c->req->param('term'), $c ); + + my ($response, @addresses, @locations); + + if ( $lat && $long ) { + $response = { latitude => $lat, longitude => $long }; + } else { + if ( ref($suggestions) eq 'ARRAY' ) { + foreach (@$suggestions) { + push @addresses, decode_utf8($_->{address}); + push @locations, { address => decode_utf8($_->{address}), lat => $_->{latitude}, long => $_->{longitude} }; + } + $response = { suggestions => \@addresses, locations => \@locations }; + } else { + $response = { error => $suggestions }; + } + } + + if ( $c->stash->{allow_single_geocode_match_strings} ) { + $response = \@addresses; + } + + my $body = JSON->new->utf8(1)->encode( + $response + ); + $c->res->body($body); + +} + __PACKAGE__->meta->make_immutable; 1; diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 3dc25dedf..5a4243fbf 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -157,11 +157,8 @@ sub token : Path('/M') : Args(1) { # Sign out in case we are another user $c->logout(); - # get the email and scrap the token - my $data = $token_obj->data; - $token_obj->delete; - # find or create the user related to the token. + my $data = $token_obj->data; my $user = $c->model('DB::User')->find_or_create( { email => $data->{email} } ); $user->name( $data->{name} ) if $data->{name}; $user->password( $data->{password}, 1 ) if $data->{password}; @@ -182,6 +179,10 @@ Used after signing in to take the person back to where they were. sub redirect_on_signin : Private { my ( $self, $c, $redirect ) = @_; $redirect = 'my' unless $redirect; + if ( $c->cobrand->moniker eq 'zurich' ) { + $redirect = 'my' if $redirect eq 'admin'; + $redirect = 'admin' if $c->user->from_body; + } $c->res->redirect( $c->uri_for( "/$redirect" ) ); } diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm index 926a3f2a5..6bc6e90ef 100644 --- a/perllib/FixMyStreet/App/Controller/Contact.pm +++ b/perllib/FixMyStreet/App/Controller/Contact.pm @@ -108,11 +108,19 @@ sub validate : Private { if !mySociety::EmailUtil::is_valid_email( $c->req->param('em') ); } + %field_errors = ( + %field_errors, + $c->cobrand->extra_contact_validation($c) + ); + push @errors, _('Illegal ID') if $c->req->param('id') && $c->req->param('id') !~ /^[1-9]\d*$/ or $c->req->param('update_id') && $c->req->param('update_id') !~ /^[1-9]\d*$/; + push @errors, _('There was a problem showing this page. Please try again later.') + if $c->req->params->{message} && $c->req->params->{message} =~ /\[url=|<a/; + unshift @errors, _('There were problems with your report. Please see below.') if scalar keys %field_errors; @@ -146,7 +154,7 @@ sub prepare_params_for_email : Private { my $problem_url = $base_url . '/report/' . $c->stash->{update}->problem_id . '#update_' . $c->stash->{update}->id; - my $admin_url = " - $admin_url" . 'update_edit/' . $c->stash->{update}->id + my $admin_url = " - $admin_url" . '/update_edit/' . $c->stash->{update}->id if $admin_url; $c->stash->{message} .= sprintf( " \n\n[ Complaint about update %d on report %d - %s%s ]", @@ -158,7 +166,7 @@ sub prepare_params_for_email : Private { elsif ( $c->stash->{problem} ) { my $problem_url = $base_url . '/report/' . $c->stash->{problem}->id; - $admin_url = " - $admin_url" . 'report_edit/' . $c->stash->{problem}->id + $admin_url = " - $admin_url" . '/report_edit/' . $c->stash->{problem}->id if $admin_url; $c->stash->{message} .= sprintf( " \n\n[ Complaint about report %d - %s%s ]", @@ -184,7 +192,7 @@ generally required to stash sub setup_request : Private { my ( $self, $c ) = @_; - $c->stash->{contact_email} = $c->cobrand->contact_email( 'contact' ); + $c->stash->{contact_email} = $c->cobrand->contact_email; $c->stash->{contact_email} =~ s/\@/@/; for my $param (qw/em subject message/) { @@ -206,7 +214,7 @@ Sends the email sub send_email : Private { my ( $self, $c ) = @_; - my $recipient = $c->cobrand->contact_email( 'contact' ); + my $recipient = $c->cobrand->contact_email; my $recipient_name = $c->cobrand->contact_name(); $c->stash->{host} = $c->req->header('HOST'); diff --git a/perllib/FixMyStreet/App/Controller/Council.pm b/perllib/FixMyStreet/App/Controller/Council.pm index cb9e78421..ceec04027 100644 --- a/perllib/FixMyStreet/App/Controller/Council.pm +++ b/perllib/FixMyStreet/App/Controller/Council.pm @@ -14,34 +14,34 @@ Catalyst Controller. =head1 METHODS -=head2 load_and_check_councils_and_wards +=head2 load_and_check_areas_and_wards -Try to load councils and wards for this location and check that we have at least one. If -there are no councils then return false. +Try to load areas and wards for this location and check that we have at least one. If +there are no areas then return false. =cut -sub load_and_check_councils_and_wards : Private { +sub load_and_check_areas_and_wards : Private { my ( $self, $c ) = @_; my $area_types = [ @{$c->cobrand->area_types}, @{$c->cobrand->area_types_children} ]; $c->stash->{area_types} = $area_types; - $c->forward('load_and_check_councils'); + $c->forward('load_and_check_areas'); } -=head2 load_and_check_councils +=head2 load_and_check_areas -Try to load councils for this location and check that we have at least one. If -there are no councils then return false. +Try to load areas for this location and check that we have at least one. If +there are no areas then return false. =cut -sub load_and_check_councils : Private { +sub load_and_check_areas : Private { my ( $self, $c ) = @_; my $latitude = $c->stash->{latitude}; my $longitude = $c->stash->{longitude}; - # Look up councils and do checks for the point we've got + # Look up areas and do checks for the point we've got my $area_types; if ( $c->stash->{area_types} and scalar @{ $c->stash->{area_types} } ) { $area_types = $c->stash->{area_types}; @@ -52,49 +52,52 @@ sub load_and_check_councils : Private { my $short_latitude = Utils::truncate_coordinate($latitude); my $short_longitude = Utils::truncate_coordinate($longitude); - my $all_councils; + my $all_areas; if ( $c->stash->{fetch_all_areas} ) { my %area_types = map { $_ => 1 } @$area_types; - my $all_areas = + $all_areas = mySociety::MaPit::call( 'point', "4326/$short_longitude,$short_latitude" ); - $c->stash->{all_areas} = $all_areas; - $all_councils = { + $c->stash->{all_areas_mapit} = $all_areas; + $all_areas = { map { $_ => $all_areas->{$_} } grep { $area_types{ $all_areas->{$_}->{type} } } keys %$all_areas }; } else { - $all_councils = + $all_areas = mySociety::MaPit::call( 'point', "4326/$short_longitude,$short_latitude", type => $area_types ); } - if ($all_councils->{error}) { - $c->stash->{location_error} = $all_councils->{error}; + if ($all_areas->{error}) { + $c->stash->{location_error_mapit_error} = 1; + $c->stash->{location_error} = $all_areas->{error}; return; } # Let cobrand do a check my ( $success, $error_msg ) = - $c->cobrand->council_check( { all_councils => $all_councils }, - $c->stash->{council_check_action} ); + $c->cobrand->area_check( { all_areas => $all_areas }, + $c->stash->{area_check_action} ); if ( !$success ) { + $c->stash->{location_error_cobrand_check} = 1; $c->stash->{location_error} = $error_msg; return; } # edit hash in-place - $c->cobrand->remove_redundant_councils($all_councils) if $c->stash->{remove_redundant_councils}; + $c->cobrand->remove_redundant_areas($all_areas) if $c->stash->{remove_redundant_areas}; - # If we don't have any councils we can't accept the report - if ( !scalar keys %$all_councils ) { + # If we don't have any areas we can't accept the report + if ( !scalar keys %$all_areas ) { + $c->stash->{location_error_no_areas} = 1; $c->stash->{location_error} = _('That location does not appear to be covered by a council; perhaps it is offshore or outside the country. Please try again.'); return; } - # all good if we have some councils left - $c->stash->{all_councils} = $all_councils; - $c->stash->{all_council_names} = - [ map { $_->{name} } values %$all_councils ]; + # all good if we have some areas left + $c->stash->{all_areas} = $all_areas; + $c->stash->{all_area_names} = + [ map { $_->{name} } values %$all_areas ]; return 1; } diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm index a5ba8ff07..25c6e1923 100644 --- a/perllib/FixMyStreet/App/Controller/Dashboard.pm +++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm @@ -29,7 +29,7 @@ sub example : Local : Args(0) { } # TODO Set up manual version of what the below would do - #$c->forward( '/report/new/setup_categories_and_councils' ); + #$c->forward( '/report/new/setup_categories_and_bodies' ); # See if we've had anything from the dropdowns - perhaps vary results if so $c->stash->{ward} = $c->req->param('ward'); @@ -74,9 +74,9 @@ sub check_page_allowed : Private { $c->detach( '/auth/redirect' ) unless $c->user_exists; $c->detach( '/page_error_404_not_found' ) - unless $c->user_exists && $c->user->from_council; + unless $c->user_exists && $c->user->from_body; - return $c->user->from_council; + return $c->user->from_body; } =head2 index @@ -88,20 +88,23 @@ Show the dashboard table. sub index : Path : Args(0) { my ( $self, $c ) = @_; - my $council = $c->forward('check_page_allowed'); + my $body = $c->forward('check_page_allowed'); # Set up the data for the dropdowns - my $council_detail = mySociety::MaPit::call('area', $council ); + # Just take the first area ID we find + my $area_id = $body->body_areas->first->area_id; + + my $council_detail = mySociety::MaPit::call('area', $area_id ); $c->stash->{council} = $council_detail; - my $children = mySociety::MaPit::call('area/children', $council, + my $children = mySociety::MaPit::call('area/children', $area_id, type => $c->cobrand->area_types_children, ); $c->stash->{children} = $children; - $c->stash->{all_councils} = { $council => $council_detail }; - $c->forward( '/report/new/setup_categories_and_councils' ); + $c->stash->{all_areas} = { $area_id => $council_detail }; + $c->forward( '/report/new/setup_categories_and_bodies' ); # See if we've had anything from the dropdowns @@ -109,7 +112,7 @@ sub index : Path : Args(0) { $c->stash->{category} = $c->req->param('category'); my %where = ( - council => $council, # XXX This will break in a two tier council. Restriction needs looking at... + bodies_str => $body->id, # XXX Does this break in a two tier council? Restriction needs looking at... 'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ], ); $where{areas} = { 'like', '%,' . $c->stash->{ward} . ',%' } @@ -118,16 +121,23 @@ sub index : Path : Args(0) { if $c->stash->{category}; $c->stash->{where} = \%where; my $prob_where = { %where }; - $prob_where->{state} = $prob_where->{'problem.state'}; + $prob_where->{'me.state'} = $prob_where->{'problem.state'}; delete $prob_where->{'problem.state'}; $c->stash->{prob_where} = $prob_where; + my $dtf = $c->model('DB')->storage->datetime_parser; + my %counts; - my $t = DateTime->today; - $counts{wtd} = $c->forward( 'updates_search', [ $t->subtract( days => $t->dow - 1 ) ] ); - $counts{week} = $c->forward( 'updates_search', [ DateTime->now->subtract( weeks => 1 ) ] ); - $counts{weeks} = $c->forward( 'updates_search', [ DateTime->now->subtract( weeks => 4 ) ] ); - $counts{ytd} = $c->forward( 'updates_search', [ DateTime->today->set( day => 1, month => 1 ) ] ); + my $now = DateTime->now( time_zone => 'local' ); + my $t = $now->clone->truncate( to => 'day' ); + $counts{wtd} = $c->forward( 'updates_search', + [ $dtf->format_datetime( $t->clone->subtract( days => $t->dow - 1 ) ) ] ); + $counts{week} = $c->forward( 'updates_search', + [ $dtf->format_datetime( $now->clone->subtract( weeks => 1 ) ) ] ); + $counts{weeks} = $c->forward( 'updates_search', + [ $dtf->format_datetime( $now->clone->subtract( weeks => 4 ) ) ] ); + $counts{ytd} = $c->forward( 'updates_search', + [ $dtf->format_datetime( $t->clone->set( day => 1, month => 1 ) ) ] ); $c->stash->{problems} = \%counts; @@ -135,26 +145,126 @@ sub index : Path : Args(0) { $c->stash->{q_state} = $c->req->param('state') || ''; if ( $c->stash->{q_state} eq 'fixed' ) { - $prob_where->{state} = [ FixMyStreet::DB::Result::Problem->fixed_states() ]; + $prob_where->{'me.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ]; } elsif ( $c->stash->{q_state} ) { - $prob_where->{state} = $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, - 'me.confirmed' => { '>=', DateTime->now->subtract( days => 30 ) }, + 'me.confirmed' => { '>=', $dtf->format_datetime( $now->clone->subtract( days => 30 ) ) }, }; - my @problems = $c->cobrand->problems->search( $params )->all; + my $problems_rs = $c->cobrand->problems->search( $params ); + my @problems = $problems_rs->all; + my %problems; foreach (@problems) { - if ($_->confirmed >= DateTime->now->subtract(days => 7)) { + if ($_->confirmed >= $now->clone->subtract(days => 7)) { push @{$problems{1}}, $_; - } elsif ($_->confirmed >= DateTime->now->subtract(days => 14)) { + } elsif ($_->confirmed >= $now->clone->subtract(days => 14)) { push @{$problems{2}}, $_; } else { push @{$problems{3}}, $_; } } $c->stash->{lists} = \%problems; + + if ( $c->req->params->{export} ) { + $self->export_as_csv($c, $problems_rs, $body); + } +} + +sub export_as_csv { + my ($self, $c, $problems_rs, $body) = @_; + require Text::CSV; + my $problems = $problems_rs->search( + {}, { prefetch => 'comments' }); + + my $filename = do { + my %where = ( + body => $body->id, + category => $c->stash->{category}, + state => $c->stash->{q_state}, + ward => $c->stash->{ward}, + ); + join '-', + $c->req->uri->host, + map { + my $value = $where{$_}; + (defined $value and length $value) ? ($_, $value) : () + } sort keys %where }; + + my $csv = Text::CSV->new({ binary => 1, eol => "\n" }); + $csv->combine( + 'Report ID', + 'Title', + 'Detail', + 'User Name', + 'Category', + 'Created', + 'Confirmed', + 'Acknowledged', + 'Fixed', + 'Closed', + 'Status', + 'Latitude', 'Longitude', + 'Nearest Postcode', + 'Report URL', + ); + my @body = ($csv->string); + + my $fixed_states = FixMyStreet::DB::Result::Problem->fixed_states; + my $closed_states = FixMyStreet::DB::Result::Problem->closed_states; + + while ( my $report = $problems->next ) { + my $external_body; + my $body_name = ""; + if ( $external_body = $report->body($c) ) { + # seems to be a zurich specific thing + $body_name = $external_body->name if ref $external_body; + } + my $hashref = $report->as_hashref($c); + + $hashref->{user_name_display} = $report->anonymous? + '(anonymous)' : $report->user->name; + + for my $comment ($report->comments) { + my $problem_state = $comment->problem_state or next; + next if $problem_state eq 'confirmed'; + $hashref->{acknowledged_pp} //= $c->cobrand->prettify_dt( $comment->created ); + $hashref->{fixed_pp} //= $fixed_states->{ $problem_state } ? + $c->cobrand->prettify_dt( $comment->created ): undef; + if ($closed_states->{ $problem_state }) { + $hashref->{closed_pp} = $c->cobrand->prettify_dt( $comment->created ); + last; + } + } + + $csv->combine( + @{$hashref}{ + 'id', + 'title', + 'detail', + 'user_name_display', + 'category', + 'created_pp', + 'confirmed_pp', + 'acknowledged_pp', + 'fixed_pp', + 'closed_pp', + 'state', + 'latitude', 'longitude', + 'postcode', + }, + (join '', $c->cobrand->base_url_for_report($report), $report->url), + ); + + push @body, $csv->string; + } + $c->res->content_type('text/csv; charset=utf-8'); + $c->res->header('content-disposition' => "attachment; filename=${filename}.csv"); + $c->res->body( join "", @body ); } sub updates_search : Private { @@ -181,11 +291,13 @@ sub updates_search : Private { map { $_ => $counts{$_} || 0 } ('confirmed', 'investigating', 'in progress', 'closed', 'fixed - council', 'fixed - user', 'fixed', 'unconfirmed', 'hidden', - 'partial', 'planned'); + 'partial', 'action scheduled', 'planned'); + + $counts{'action scheduled'} += $counts{planned} || 0; for my $vars ( [ 'time_to_fix', 'fixed - council' ], - [ 'time_to_mark', 'in progress', 'planned', 'investigating', 'closed' ], + [ 'time_to_mark', 'in progress', 'action scheduled', 'investigating', 'closed' ], ) { my $col = shift @$vars; my $substmt = "select min(id) from comment where me.problem_id=comment.problem_id and problem_state in ('" diff --git a/perllib/FixMyStreet/App/Controller/FakeMapit.pm b/perllib/FixMyStreet/App/Controller/FakeMapit.pm index bc46df712..253c75ba4 100755 --- a/perllib/FixMyStreet/App/Controller/FakeMapit.pm +++ b/perllib/FixMyStreet/App/Controller/FakeMapit.pm @@ -12,13 +12,13 @@ FixMyStreet::App::Controller::FakeMapit - Catalyst Controller A controller to fake mapit when we don't have it. If you set MAPIT_URL to .../fakemapit/ it should all just work, with a mapit that assumes the whole -world is one area, with ID 161 and name "Default Area". +world is one area, with ID 161 and name "Everywhere". =head1 METHODS =cut -my $area = { "name" => "Default Area", "type" => "ZZZ", "id" => 161 }; +my $area = { "name" => "Everywhere", "type" => "ZZZ", "id" => 161 }; sub output : Private { my ( $self, $c, $data ) = @_; diff --git a/perllib/FixMyStreet/App/Controller/JS.pm b/perllib/FixMyStreet/App/Controller/JS.pm index d7847af75..483c3c2cc 100755 --- a/perllib/FixMyStreet/App/Controller/JS.pm +++ b/perllib/FixMyStreet/App/Controller/JS.pm @@ -11,13 +11,13 @@ FixMyStreet::App::Controller::JS - Catalyst Controller =head1 DESCRIPTION JS Catalyst Controller. To return a language-dependent list -of validation strings. +of translation strings. =head1 METHODS =cut -sub validation_strings : LocalRegex('^validation_strings\.(.*?)\.js$') : Args(0) { +sub translation_strings : LocalRegex('^translation_strings\.(.*?)\.js$') : Args(0) { my ( $self, $c ) = @_; my $lang = $c->req->captures->[0]; $c->cobrand->set_lang_and_domain( $lang, 1 ); diff --git a/perllib/FixMyStreet/App/Controller/JSON.pm b/perllib/FixMyStreet/App/Controller/JSON.pm index f3607341a..17507a84b 100644 --- a/perllib/FixMyStreet/App/Controller/JSON.pm +++ b/perllib/FixMyStreet/App/Controller/JSON.pm @@ -8,6 +8,7 @@ use JSON; use DateTime; use DateTime::Format::ISO8601; use List::MoreUtils 'uniq'; +use FixMyStreet::App; =head1 NAME @@ -80,11 +81,13 @@ sub problems : Local { $date_col = 'lastupdate'; } + my $dt_parser = FixMyStreet::App->model('DB')->schema->storage->datetime_parser; + my $one_day = DateTime::Duration->new( days => 1 ); my $query = { $date_col => { - '>=' => $start_dt, - '<=' => $end_dt + $one_day, + '>=' => $dt_parser->format_datetime($start_dt), + '<=' => $dt_parser->format_datetime($end_dt + $one_day), }, state => [ @state ], }; @@ -92,7 +95,7 @@ sub problems : Local { my @problems = $c->cobrand->problems->search( $query, { order_by => { -asc => 'confirmed' }, columns => [ - 'id', 'title', 'council', 'category', + 'id', 'title', 'bodies_str', 'category', 'detail', 'name', 'anonymous', 'confirmed', 'whensent', 'service', 'latitude', 'longitude', 'used_map', @@ -100,23 +103,13 @@ sub problems : Local { ] } ); - my @councils; foreach my $problem (@problems) { $problem->name( '' ) if $problem->anonymous == 1; $problem->service( 'Web interface' ) if $problem->service eq ''; - if ($problem->council) { - (my $council = $problem->council) =~ s/\|.*//g; - my @council_ids = split /,/, $council; - push(@councils, @council_ids); - $problem->council( \@council_ids ); - } - } - @councils = uniq @councils; - my $areas_info = mySociety::MaPit::call('areas', \@councils); - foreach my $problem (@problems) { - if ($problem->council) { - my @council_names = map { $areas_info->{$_}->{name} } @{$problem->council} ; - $problem->council( join(' and ', @council_names) ); + my $bodies = $problem->bodies; + if (keys %$bodies) { + my @body_names = map { $_->name } values %$bodies; + $problem->bodies_str( join(' and ', @body_names) ); } } diff --git a/perllib/FixMyStreet/App/Controller/Location.pm b/perllib/FixMyStreet/App/Controller/Location.pm index e8bf2cd1c..4312b6911 100644 --- a/perllib/FixMyStreet/App/Controller/Location.pm +++ b/perllib/FixMyStreet/App/Controller/Location.pm @@ -5,6 +5,7 @@ use namespace::autoclean; BEGIN {extends 'Catalyst::Controller'; } use Encode; +use FixMyStreet::Geocode; =head1 NAME @@ -49,6 +50,8 @@ sub determine_location_from_coords : Private { User has searched for a location - try to find it for them. +Return -1 if nothing provided. + If one match is found returns true and lat/lng is set. If several possible matches are found puts an array onto stash so that user can be prompted to pick one and returns false. @@ -61,7 +64,7 @@ sub determine_location_from_pc : Private { my ( $self, $c, $pc ) = @_; # check for something to search - $pc ||= $c->req->param('pc') || return; + $pc ||= $c->req->param('pc') || return -1; $c->stash->{pc} = $pc; # for template if ( $pc =~ /^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/ ) { @@ -100,6 +103,7 @@ sub determine_location_from_pc : Private { } # pass errors back to the template + $c->stash->{location_error_pc_lookup} = 1; $c->stash->{location_error} = $error; return; } diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm index c00264315..bbef1f8d8 100644 --- a/perllib/FixMyStreet/App/Controller/My.pm +++ b/perllib/FixMyStreet/App/Controller/My.pm @@ -45,6 +45,7 @@ sub my : Path : Args(0) { } )->page( $p_page ); while ( my $problem = $rs->next ) { + $c->stash->{has_content}++; push @$pins, { latitude => $problem->latitude, longitude => $problem->longitude, @@ -64,7 +65,9 @@ sub my : Path : Args(0) { order_by => { -desc => 'confirmed' }, rows => 50 } )->page( $u_page ); + my @updates = $rs->all; + $c->stash->{has_content} += scalar @updates; $c->stash->{updates} = \@updates; $c->stash->{updates_pager} = $rs->pager; diff --git a/perllib/FixMyStreet/App/Controller/Open311.pm b/perllib/FixMyStreet/App/Controller/Open311.pm index 040b0d3e6..f3841acef 100644 --- a/perllib/FixMyStreet/App/Controller/Open311.pm +++ b/perllib/FixMyStreet/App/Controller/Open311.pm @@ -100,7 +100,7 @@ sub error : Private { sub get_discovery : Private { my ( $self, $c ) = @_; - my $contact_email = $c->config->{CONTACT_EMAIL}; + my $contact_email = $c->cobrand->contact_email; my $prod_url = 'http://www.fiksgatami.no/open311'; my $test_url = 'http://fiksgatami-dev.nuug.no/open311'; my $prod_changeset = '2011-04-08T00:00:00Z'; @@ -164,12 +164,12 @@ sub get_services : Private { if ($lat || $lon) { my $area_types = $c->cobrand->area_types; - my $all_councils = mySociety::MaPit::call('point', + my $all_areas = mySociety::MaPit::call('point', "4326/$lon,$lat", type => $area_types); $categories = $categories->search( { - area_id => [ keys %$all_councils ], - } ); + 'body_areas.area_id' => [ keys %$all_areas ], + }, { join => { 'body' => 'body_areas' } } ); } my @categories = $categories->search( undef, { @@ -223,19 +223,11 @@ sub output_requests : Private { ); my @problemlist; - my @councils; while ( my $problem = $problems->next ) { my $id = $problem->id; $problem->service( 'Web interface' ) unless $problem->service; - if ($problem->council) { - (my $council = $problem->council) =~ s/\|.*//g; - my @council_ids = split(/,/, $council); - push(@councils, @council_ids); - $problem->council( \@council_ids ); - } - $problem->state( $statusmap{$problem->state} ); my $request = @@ -248,19 +240,23 @@ sub output_requests : Private { 'long' => [ $problem->longitude ], 'status' => [ $problem->state ], # 'status_notes' => [ {} ], - 'requested_datetime' => [ w3date($problem->confirmed_local) ], - 'updated_datetime' => [ w3date($problem->lastupdate_local) ], + 'requested_datetime' => [ w3date($problem->confirmed) ], + 'updated_datetime' => [ w3date($problem->lastupdate) ], # 'expected_datetime' => [ {} ], # 'address' => [ {} ], # 'address_id' => [ {} ], 'service_code' => [ $problem->category ], 'service_name' => [ $problem->category ], # 'service_notice' => [ {} ], - 'agency_responsible' => $problem->council , # FIXME Not according to Open311 v2 # 'zipcode' => [ {} ], 'interface_used' => [ $problem->service ], # Not in Open311 v2 }; + if ( $c->cobrand->moniker ne 'zurich' ) { # XXX + # FIXME Not according to Open311 v2 + $request->{agency_responsible} = $problem->bodies; + } + if ( !$problem->anonymous ) { # Not in Open311 v2 $request->{'requestor_name'} = [ $problem->name ]; @@ -268,7 +264,7 @@ sub output_requests : Private { if ( $problem->whensent ) { # Not in Open311 v2 $request->{'agency_sent_datetime'} = - [ w3date($problem->whensent_local) ]; + [ w3date($problem->whensent) ]; } # Extract number of updates @@ -280,7 +276,7 @@ sub output_requests : Private { $request->{'comment_count'} = [ $updates ]; } - my $display_photos = $c->cobrand->allow_photo_display; + my $display_photos = $c->cobrand->allow_photo_display($problem); if ($display_photos && $problem->photo) { my $url = $c->cobrand->base_url(); my $imgurl = $url . "/photo/$id.full.jpeg"; @@ -288,12 +284,12 @@ sub output_requests : Private { } push(@problemlist, $request); } - my $areas_info = mySociety::MaPit::call('areas', \@councils); + foreach my $request (@problemlist) { if ($request->{agency_responsible}) { - my @council_names = map { $areas_info->{$_}->{name} } @{$request->{agency_responsible}} ; + my @body_names = map { $_->name } values %{$request->{agency_responsible}} ; $request->{agency_responsible} = - [ {'recipient' => [ @council_names ] } ]; + [ {'recipient' => [ @body_names ] } ]; } } $c->forward( 'format_output', [ { @@ -311,15 +307,17 @@ sub get_requests : Private { my $max_requests = $c->req->param('max_requests') || 0; # Only provide access to the published reports + my $states = FixMyStreet::DB::Result::Problem->visible_states(); + delete $states->{unconfirmed}; my $criteria = { - state => [ FixMyStreet::DB::Result::Problem->visible_states() ] + state => [ keys %$states ] }; my %rules = ( service_request_id => [ '=', 'id' ], service_code => [ '=', 'category' ], status => [ 'IN', 'state' ], - agency_responsible => [ '~', 'council' ], + agency_responsible => [ '~', 'bodies_str' ], interface_used => [ '=', 'service' ], has_photo => [ '=', 'photo' ], ); @@ -411,8 +409,10 @@ sub get_request : Private { return; } + my $states = FixMyStreet::DB::Result::Problem->visible_states(); + delete $states->{unconfirmed}; my $criteria = { - state => [ FixMyStreet::DB::Result::Problem->visible_states() ], + state => [ keys %$states ], id => $id, }; $c->forward( 'output_requests', [ $criteria ] ); diff --git a/perllib/FixMyStreet/App/Controller/Photo.pm b/perllib/FixMyStreet/App/Controller/Photo.pm index fa4baf045..09afabecf 100644 --- a/perllib/FixMyStreet/App/Controller/Photo.pm +++ b/perllib/FixMyStreet/App/Controller/Photo.pm @@ -5,7 +5,7 @@ use namespace::autoclean; BEGIN {extends 'Catalyst::Controller'; } use DateTime::Format::HTTP; -use Digest::SHA1 qw(sha1_hex); +use Digest::SHA qw(sha1_hex); use File::Path; use File::Slurp; use Path::Class; @@ -30,17 +30,19 @@ Display a photo =cut -sub during :LocalRegex('^([0-9a-f]{40})\.temp\.jpeg$') { +sub during :LocalRegex('^([0-9a-f]{40})\.(temp|fulltemp)\.jpeg$') { my ( $self, $c ) = @_; - my ( $hash ) = @{ $c->req->captures }; + my ( $hash, $size ) = @{ $c->req->captures }; my $file = file( $c->config->{UPLOAD_DIR}, "$hash.jpeg" ); my $photo = $file->slurp; - if ( $c->cobrand->default_photo_resize ) { - $photo = _shrink( $photo, $c->cobrand->default_photo_resize ); - } else { - $photo = _shrink( $photo, '250x250' ); + if ( $size eq 'temp' ) { + if ( $c->cobrand->default_photo_resize ) { + $photo = _shrink( $photo, $c->cobrand->default_photo_resize ); + } else { + $photo = _shrink( $photo, '250x250' ); + } } $c->forward( 'output', [ $photo ] ); @@ -73,7 +75,9 @@ sub index :LocalRegex('^(c/)?(\d+)(?:\.(full|tn|fp))?\.jpeg$') { $c->detach( 'no_photo' ) unless @photo; - my $photo = $photo[0]->photo; + my $item = $photo[0]; + $c->detach( 'no_photo' ) unless $c->cobrand->allow_photo_display($item); # Should only be for reports, not updates + my $photo = $item->photo; # If photo field contains a hash if (length($photo) == 40) { @@ -102,10 +106,7 @@ sub output : Private { File::Path::make_path( FixMyStreet->path_to( 'web', 'photo', 'c' )->stringify ); File::Slurp::write_file( FixMyStreet->path_to( 'web', $c->req->path )->stringify, \$photo ); - my $dt = DateTime->now()->add( years => 1 ); - $c->res->content_type( 'image/jpeg' ); - $c->res->header( 'expires', DateTime::Format::HTTP->format_datetime( $dt ) ); $c->res->body( $photo ); } diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm index ef966a8a8..13a347a90 100644 --- a/perllib/FixMyStreet/App/Controller/Report.pm +++ b/perllib/FixMyStreet/App/Controller/Report.pm @@ -51,6 +51,25 @@ sub display : Path('') : Args(1) { return $c->res->redirect( $c->uri_for($1), 301 ); } + $c->forward( '_display', [ $id ] ); +} + +=head2 ajax + +Return JSON formatted details of a report + +=cut + +sub ajax : Path('ajax') : Args(1) { + my ( $self, $c, $id ) = @_; + + $c->stash->{ajax} = 1; + $c->forward( '_display', [ $id ] ); +} + +sub _display : Private { + my ( $self, $c, $id ) = @_; + $c->forward( 'load_problem_or_display_error', [ $id ] ); $c->forward( 'load_updates' ); $c->forward( 'format_problem_for_display' ); @@ -66,7 +85,7 @@ sub support : Path('support') : Args(0) { ? $c->uri_for( '/report', $id ) : $c->uri_for('/'); - if ( $id && $c->cobrand->can_support_problems && $c->user && $c->user->from_council ) { + if ( $id && $c->cobrand->can_support_problems && $c->user && $c->user->from_body ) { $c->forward( 'load_problem_or_display_error', [ $id ] ); $c->stash->{problem}->update( { interest_count => \'interest_count +1' } ); } @@ -83,7 +102,7 @@ sub load_problem_or_display_error : Private { : $c->cobrand->problems->find( { id => $id } ); # check that the problem is suitable to show. - if ( !$problem || $problem->state eq 'unconfirmed' || $problem->state eq 'partial' ) { + if ( !$problem || ($problem->state eq 'unconfirmed' && !$c->cobrand->show_unconfirmed_reports) || $problem->state eq 'partial' ) { $c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] ); } elsif ( $problem->state eq 'hidden' ) { @@ -147,10 +166,25 @@ sub format_problem_for_display : Private { $c->stash->{add_alert} = 1; } - $c->stash->{extra_name_info} = $problem->council && $problem->council eq '2482' ? 1 : 0; + $c->stash->{extra_name_info} = $problem->bodies_str && $problem->bodies_str eq '2482' ? 1 : 0; + if ( $c->sessionid && $c->flash->{created_report} ) { + $c->stash->{created_report} = $c->flash->{created_report}; + } $c->forward('generate_map_tags'); + if ( $c->stash->{ajax} ) { + $c->res->content_type('application/json; charset=utf-8'); + my $content = JSON->new->utf8(1)->encode( + { + report => $c->cobrand->problem_as_hashref( $problem, $c ), + updates => $c->cobrand->updates_as_hashref( $problem, $c ), + } + ); + $c->res->body( $content ); + return 1; + } + return 1; } @@ -168,7 +202,7 @@ sub generate_map_tags : Private { ? [ { latitude => $problem->latitude, longitude => $problem->longitude, - colour => 'yellow', + colour => $c->cobrand->pin_colour($problem, 'report'), type => 'big', } ] : [], @@ -187,11 +221,10 @@ sub delete :Local :Args(1) { return $c->res->redirect($uri) unless $c->user_exists; - my $council = $c->user->obj->from_council; - return $c->res->redirect($uri) unless $council; + my $body = $c->user->obj->from_body; + return $c->res->redirect($uri) unless $body; - my %councils = map { $_ => 1 } @{$p->councils}; - return $c->res->redirect($uri) unless $councils{$council}; + return $c->res->redirect($uri) unless $p->bodies->{$body->id}; $p->state('hidden'); $p->lastupdate( \'ms_current_timestamp()' ); diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index 169c3d152..1e9f83aec 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -4,7 +4,6 @@ use Moose; use namespace::autoclean; BEGIN { extends 'Catalyst::Controller'; } -use FixMyStreet::Geocode; use Encode; use List::MoreUtils qw(uniq); use POSIX 'strcoll'; @@ -90,7 +89,7 @@ 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_councils'); + $c->forward('setup_categories_and_bodies'); $c->forward('generate_map'); $c->forward('check_for_category'); @@ -120,7 +119,7 @@ sub report_new_ajax : Path('mobile') : Args(0) { return 1; } - $c->forward('setup_categories_and_councils'); + $c->forward('setup_categories_and_bodies'); $c->forward('process_user'); $c->forward('process_report'); $c->forward('/photo/process_photo'); @@ -148,7 +147,7 @@ sub report_new_ajax : Path('mobile') : Args(0) { } else { $c->stash->{token_url} = $c->uri_for_email( '/P', $token->token ); $c->send_email( 'problem-confirm.txt', { - to => [ [ $report->user->email, $report->name ] ], + to => [ $report->name ? [ $report->user->email, $report->name ] : $report->user->email ], } ); $c->stash->{ json_response } = { success => 1 }; } @@ -181,7 +180,7 @@ sub report_form_ajax : Path('ajax') : Args(0) { return; } - $c->forward('setup_categories_and_councils'); + $c->forward('setup_categories_and_bodies'); # render templates to get the html my $category = $c->render_fragment( 'report/new/category.html'); @@ -190,11 +189,14 @@ sub report_form_ajax : Path('ajax') : Args(0) { ? $c->render_fragment('report/new/extra_name.html') : ''; + my $extra_titles_list = $c->cobrand->title_list($c->stash->{all_areas}); + my $body = JSON->new->utf8(1)->encode( { councils_text => $councils_text, category => $category, extra_name_info => $extra_name_info, + titles_list => $extra_titles_list, categories => $c->stash->{category_options}, } ); @@ -217,7 +219,7 @@ sub category_extras_ajax : Path('category_extras') : Args(0) { $c->res->body($body); return 1; } - $c->forward('setup_categories_and_councils'); + $c->forward('setup_categories_and_bodies'); my $category_extra = ''; if ( $c->stash->{category_extras}->{ $c->req->param('category') } && @{ $c->stash->{category_extras}->{ $c->req->param('category') } } >= 1 ) { @@ -570,29 +572,35 @@ sub determine_location_from_report : Private { return; } -=head2 setup_categories_and_councils +=head2 setup_categories_and_bodies -Look up categories for this council or councils +Look up categories for the relevant body or bodies. =cut -sub setup_categories_and_councils : Private { +sub setup_categories_and_bodies : Private { my ( $self, $c ) = @_; - my $all_councils = $c->stash->{all_councils}; - my $first_council = ( values %$all_councils )[0]; + my $all_areas = $c->stash->{all_areas}; + my $first_area = ( values %$all_areas )[0]; + + my @bodies = $c->model('DB::Body')->search( + { 'body_areas.area_id' => [ keys %$all_areas ], deleted => 0 }, + { join => 'body_areas' } + )->all; + my %bodies = map { $_->id => $_ } @bodies; + my $first_body = ( values %bodies )[0]; my @contacts # = $c # ->model('DB::Contact') # ->not_deleted # - ->search( { area_id => [ keys %$all_councils ] } ) # + ->search( { body_id => [ keys %bodies ] } ) ->all; # variables to populate - my %area_ids_to_list = (); # Areas with categories assigned + my %bodies_to_list = (); # Bodies with categories assigned my @category_options = (); # categories to show - my $category_label = undef; # what to call them my %category_extras = (); # extra fields to fill in for open311 my %non_public_categories = (); # categories for which the reports are not public @@ -600,9 +608,9 @@ sub setup_categories_and_councils : Private { # FIXME - implement in cobrand if ( $c->cobrand->moniker eq 'emptyhomes' ) { - # add all areas found to the list + # add all bodies found to the list foreach (@contacts) { - $area_ids_to_list{ $_->area_id } = 1; + $bodies_to_list{ $_->body_id } = 1; } # set our own categories @@ -615,22 +623,18 @@ sub setup_categories_and_councils : Private { _('Empty pub or bar'), _('Empty public building - school, hospital, etc.') ); - $category_label = _('Property type:'); - } elsif ($first_council->{id} != COUNCIL_ID_BROMLEY && $first_council->{type} eq 'LBO') { + } elsif ($first_area->{id} != COUNCIL_ID_BROMLEY + && $first_area->{id} != COUNCIL_ID_BARNET + && $first_area->{type} eq 'LBO') { - $area_ids_to_list{ $first_council->{id} } = 1; + $bodies_to_list{ $first_body->id } = 1; my @local_categories; - if ($first_council->{id} == COUNCIL_ID_BARNET) { - @local_categories = sort keys %{ Utils::barnet_categories() } - } else { - @local_categories = sort keys %{ Utils::london_categories() } - } + @local_categories = sort keys %{ Utils::london_categories() }; @category_options = ( _('-- Pick a category --'), @local_categories ); - $category_label = _('Category'); } else { @@ -640,7 +644,7 @@ sub setup_categories_and_councils : Private { my %seen; foreach my $contact (@contacts) { - $area_ids_to_list{ $contact->area_id } = 1; + $bodies_to_list{ $contact->body_id } = 1; unless ( $seen{$contact->category} ) { push @category_options, $contact->category; @@ -657,29 +661,34 @@ sub setup_categories_and_councils : Private { # 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_label = _('Category'); } } + if ($c->cobrand->can('hidden_categories')) { + my %hidden_categories = map { $_ => 1 } + $c->cobrand->hidden_categories; + + @category_options = grep { + !$hidden_categories{$_} + } @category_options; + } + # put results onto stash for display - $c->stash->{area_ids_to_list} = [ keys %area_ids_to_list ]; - $c->stash->{category_label} = $category_label; + $c->stash->{bodies} = \%bodies; + $c->stash->{all_body_names} = [ map { $_->name } values %bodies ]; + $c->stash->{all_body_urls} = [ map { $_->external_url } values %bodies ]; + $c->stash->{bodies_to_list} = [ keys %bodies_to_list ]; $c->stash->{category_options} = \@category_options; $c->stash->{category_extras} = \%category_extras; $c->stash->{non_public_categories} = \%non_public_categories; $c->stash->{category_extras_json} = encode_json \%category_extras; - $c->stash->{extra_name_info} = $first_council->{id} == COUNCIL_ID_BROMLEY ? 1 : 0; + $c->stash->{extra_name_info} = $first_area->{id} == COUNCIL_ID_BROMLEY ? 1 : 0; - my @missing_details_councils = - grep { !$area_ids_to_list{$_} } # - keys %$all_councils; + my @missing_details_bodies = grep { !$bodies_to_list{$_->id} } values %bodies; + my @missing_details_body_names = map { $_->name } @missing_details_bodies; - my @missing_details_council_names = - map { $all_councils->{$_}->{name} } # - @missing_details_councils; - - $c->stash->{missing_details_councils} = \@missing_details_councils; - $c->stash->{missing_details_council_names} = \@missing_details_council_names; + $c->stash->{missing_details_bodies} = \@missing_details_bodies; + $c->stash->{missing_details_body_names} = \@missing_details_body_names; } =head2 check_form_submitted @@ -788,6 +797,7 @@ sub process_report : Private { 'category', # 'subcategory', # 'partial', # + 'service', # ); # load the report @@ -797,6 +807,7 @@ sub process_report : Private { $report->postcode( $params{pc} ); $report->latitude( $c->stash->{latitude} ); $report->longitude( $c->stash->{longitude} ); + $report->send_questionnaire( $c->cobrand->send_questionnaires() ); # set some simple bool values (note they get inverted) $report->anonymous( $params{may_show_name} ? 0 : 1 ); @@ -812,65 +823,71 @@ sub process_report : Private { } $report->detail( $detail ); + # mobile device type + $report->service( $params{service} ) if $params{service}; + # set these straight from the params $report->category( _ $params{category} ) if $params{category}; $report->subcategory( $params{subcategory} ); - my $areas = $c->stash->{all_areas}; + my $areas = $c->stash->{all_areas_mapit}; $report->areas( ',' . join( ',', sort keys %$areas ) . ',' ); # From earlier in the process. - my $councils = $c->stash->{all_councils}; - my $first_council = ( values %$councils )[0]; + $areas = $c->stash->{all_areas}; + my $bodies = $c->stash->{bodies}; + my $first_area = ( values %$areas )[0]; + my $first_body = ( values %$bodies )[0]; if ( $c->cobrand->moniker eq 'emptyhomes' ) { - $councils = join( ',', @{ $c->stash->{area_ids_to_list} } ) || -1; - $report->council( $councils ); + $bodies = join( ',', @{ $c->stash->{bodies_to_list} } ) || -1; + $report->bodies_str( $bodies ); - } elsif ( $first_council->{id} == COUNCIL_ID_BARNET ) { - - unless ( exists Utils::barnet_categories()->{ $report->category } ) { - $c->stash->{field_errors}->{category} = _('Please choose a category'); + my %extra; + $c->cobrand->process_extras( $c, undef, \%extra ); + if ( %extra ) { + $report->extra( \%extra ); } - $report->council( $first_council->{id} ); - - } elsif ( $first_council->{id} != COUNCIL_ID_BROMLEY && $first_council->{type} eq 'LBO') { - + + } elsif ($first_area->{id} != COUNCIL_ID_BROMLEY + && $first_area->{id} != COUNCIL_ID_BARNET + && $first_area->{type} eq 'LBO') { + unless ( Utils::london_categories()->{ $report->category } ) { $c->stash->{field_errors}->{category} = _('Please choose a category'); } - $report->council( $first_council->{id} ); + $report->bodies_str( $first_body->id ); } elsif ( $report->category ) { - # FIXME All contacts were fetched in setup_categories_and_councils, + # FIXME All contacts were fetched in setup_categories_and_bodies, # so can this DB call also be avoided? my @contacts = $c-> # model('DB::Contact') # ->not_deleted # ->search( { - area_id => [ keys %$councils ], + body_id => [ keys %$bodies ], category => $report->category } )->all; unless ( @contacts ) { $c->stash->{field_errors}->{category} = _('Please choose a category'); - $report->council( -1 ); + $report->bodies_str( -1 ); return 1; } - # construct the council string: - # 'x,x' - x are council IDs that have this category - # 'x,x|y,y' - x are council IDs that have this category, y council IDs with *no* contact - my $council_string = join( ',', map { $_->area_id } @contacts ); - $council_string .= - '|' . join( ',', @{ $c->stash->{missing_details_councils} } ) - if $council_string && @{ $c->stash->{missing_details_councils} }; - $report->council($council_string); + # construct the bodies string: + # 'x,x' - x are body IDs that have this category + # 'x,x|y' - x are body IDs that have this category, y body IDs with *no* contact + my $body_string = join( ',', map { $_->body_id } @contacts ); + $body_string .= + '|' . join( ',', map { $_->id } @{ $c->stash->{missing_details_bodies} } ) + if $body_string && @{ $c->stash->{missing_details_bodies} }; + $report->bodies_str($body_string); my @extra = (); my $metas = $contacts[0]->extra; @@ -892,13 +909,13 @@ sub process_report : Private { $report->non_public( 1 ); } - $c->cobrand->process_extras( $c, $contacts[0]->area_id, \@extra ); + $c->cobrand->process_extras( $c, $contacts[0]->body_id, \@extra ); if ( @extra ) { $c->stash->{report_meta} = { map { $_->{name} => $_ } @extra }; $report->extra( \@extra ); } - } elsif ( @{ $c->stash->{area_ids_to_list} } ) { + } elsif ( @{ $c->stash->{bodies_to_list} } ) { # There was an area with categories, but we've not been given one. Bail. $c->stash->{field_errors}->{category} = _('Please choose a category'); @@ -907,7 +924,7 @@ sub process_report : Private { # If we're here, we've been submitted somewhere # where we have no contact information at all. - $report->council( -1 ); + $report->bodies_str( -1 ); } @@ -936,6 +953,22 @@ sub check_for_errors : Private { $c->stash->{field_errors} ||= {}; my %field_errors = $c->cobrand->report_check_for_errors( $c ); + # Zurich, we don't care about title or name + # There is no title, and name is optional + if ( $c->cobrand->moniker eq 'zurich' ) { + delete $field_errors{title}; + delete $field_errors{name}; + my $report = $c->stash->{report}; + $report->title( Utils::cleanup_text( substr($report->detail, 0, 25) ) ); + + # We only want to validate the phone number web requests (where the + # service parameter is blank) because previous versions of the mobile + # apps don't validate the presence of a phone number. + if ( ! $c->req->param('phone') and ! $c->req->param('service') ) { + $field_errors{phone} = _("This information is required"); + } + } + # FIXME: need to check for required bromley fields here # if they're got the login details wrong when signing in then @@ -996,8 +1029,10 @@ sub save_user_and_report : Private { $c->log->info($report->user->id . ' created for this report'); } elsif ( $c->user && $report->user->id == $c->user->id ) { + # Logged in and matches, so instantly confirm (except Zurich, with no confirmation) $report->user->update(); - $report->confirm; + $report->confirm + unless $c->cobrand->moniker eq 'zurich'; $c->log->info($report->user->id . ' is logged in for this report'); } else { @@ -1022,7 +1057,7 @@ sub save_user_and_report : Private { $report->category( _('Other') ) unless $report->category; # Set unknown to DB unknown - $report->council( undef ) if $report->council eq '-1'; + $report->bodies_str( undef ) if $report->bodies_str eq '-1'; # if there is a Message Manager message ID, pass it back to the client view if ($c->cobrand->moniker eq 'fixmybarangay' && $c->req->param('external_source_id')=~/^\d+$/) { @@ -1103,7 +1138,7 @@ sub redirect_or_confirm_creation : Private { $c->forward( 'create_reporter_alert' ); my $report_uri; - if ( $c->cobrand->moniker eq 'fixmybarangay' && $c->user->from_council && $c->stash->{external_source_id}) { + if ( $c->cobrand->moniker eq 'fixmybarangay' && $c->user->from_body && $c->stash->{external_source_id}) { $report_uri = $c->uri_for( '/report', $report->id, undef, { external_source_id => $c->stash->{external_source_id} } ); } elsif ( $c->cobrand->never_confirm_reports && $report->non_public ) { $c->log->info( 'cobrand was set to always confirm reports and report was non public, success page showed'); @@ -1113,6 +1148,9 @@ sub redirect_or_confirm_creation : Private { $report_uri = $c->cobrand->base_url_for_report( $report ) . $report->url; } $c->log->info($report->user->id . ' was logged in, redirecting to /report/' . $report->id); + if ( $c->sessionid ) { + $c->flash->{created_report} = 'loggedin'; + } $c->res->redirect($report_uri); $c->detach; } @@ -1128,7 +1166,7 @@ sub redirect_or_confirm_creation : Private { } ); $c->stash->{token_url} = $c->uri_for_email( '/P', $token->token ); $c->send_email( 'problem-confirm.txt', { - to => [ [ $report->user->email, $report->name ] ], + to => [ $report->name ? [ $report->user->email, $report->name ] : $report->user->email ], } ); # tell user that they've been sent an email diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index da4cc33ca..bc79cafd3 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -76,7 +76,7 @@ sub update_problem : Private { $problem->state('confirmed'); } - if ( $c->cobrand->can_support_problems && $c->user && $c->user->from_council && $c->req->param('external_source_id') ) { + if ( $c->cobrand->can_support_problems && $c->user && $c->user->from_body && $c->req->param('external_source_id') ) { $problem->interest_count( \'interest_count + 1' ); } @@ -201,14 +201,29 @@ sub process_update : Private { if ( $params{state} ) { $params{state} = 'fixed - council' - if $params{state} eq 'fixed' && $c->user && $c->user->belongs_to_council( $update->problem->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 + # for use when sending updates to external parties + if ( $update->mark_fixed ) { + $update->problem_state( 'fixed - user' ); + } elsif ( $update->mark_open ) { + $update->problem_state( 'confirmed' ); + # if there is not state param and neither of the above conditions apply + # then we are not changing the state of the problem so can use the current + # problem state + } else { + my $problem = $c->stash->{problem} || $update->problem; + $update->problem_state( $problem->state ); + } } + my @extra; # Next function fills this, but we don't need it here. # This is just so that the error checkign for these extra fields runs. # TODO Use extra here as it is used on reports. - $c->cobrand->process_extras( $c, $update->problem->council, \@extra ); + $c->cobrand->process_extras( $c, $update->problem->bodies_str, \@extra ); if ( $c->req->param('fms_extra_title') ) { my %extras = (); @@ -246,10 +261,11 @@ sub check_for_errors : Private { # they have to be an authority user to update the state if ( $c->req->param('state') ) { my $error = 0; - $error = 1 unless $c->user && $c->user->belongs_to_council( $c->stash->{update}->problem->council ); + $error = 1 unless $c->user && $c->user->belongs_to_body( $c->stash->{update}->problem->bodies_str ); my $state = $c->req->param('state'); - $error = 1 unless ( grep { $state eq $_ } ( qw/confirmed closed fixed investigating planned/, 'in progress', 'fixed', 'fixed - user', 'fixed - council' ) ); + $state = 'fixed - council' if $state eq 'fixed'; + $error = 1 unless ( grep { $state eq $_ } ( FixMyStreet::DB::Result::Problem->council_states() ) ); if ( $error ) { $c->stash->{errors} ||= []; @@ -296,7 +312,14 @@ sub save_update : Private { my $update = $c->stash->{update}; - if ( !$update->user->in_storage ) { + if ( $c->cobrand->never_confirm_updates ) { + if ( $update->user->in_storage() ) { + $update->user->update(); + } else { + $update->user->insert(); + } + $update->confirm(); + } elsif ( !$update->user->in_storage ) { # User does not exist. # Store changes in token for when token is validated. $c->stash->{token_data} = { @@ -354,6 +377,7 @@ sub redirect_or_confirm_creation : Private { $c->forward( 'signup_for_alerts' ); my $report_uri = $c->cobrand->base_url_for_report( $update->problem ) . $update->problem->url; + $c->flash->{comment_created} = 1; $c->res->redirect($report_uri); $c->detach; } diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm index ec41dc17f..7e0cccc7b 100644 --- a/perllib/FixMyStreet/App/Controller/Reports.pm +++ b/perllib/FixMyStreet/App/Controller/Reports.pm @@ -5,6 +5,7 @@ use namespace::autoclean; use File::Slurp; use List::MoreUtils qw(zip); use POSIX qw(strcoll); +use RABX; use mySociety::MaPit; BEGIN { extends 'Catalyst::Controller'; } @@ -30,34 +31,31 @@ Show the summary page of all reports. sub index : Path : Args(0) { my ( $self, $c ) = @_; - # Fetch all areas of the types we're interested in - my $areas_info; - eval { - my $area_types = $c->cobrand->area_types; - $areas_info = mySociety::MaPit::call('areas', $area_types, - min_generation => $c->cobrand->area_min_generation + # Zurich goes straight to map page, with all reports + if ( $c->cobrand->moniker eq 'zurich' ) { + $c->forward( 'load_and_group_problems' ); + my $pins = $c->stash->{pins}; + $c->stash->{page} = 'reports'; + FixMyStreet::Map::display_map( + $c, + latitude => @$pins ? $pins->[0]{latitude} : 0, + longitude => @$pins ? $pins->[0]{longitude} : 0, + area => 274456, + pins => $pins, + any_zoom => 1, ); - }; - if ($@) { - $c->stash->{message} = _("Unable to look up areas in MaPit. Please try again later.") . ' ' . - sprintf(_('The error was: %s'), $@); - $c->stash->{template} = 'errors/generic.html'; - return; + return 1; } - # For each area, add its link and perhaps alter its name if we need to for - # places with the same name. - foreach (values %$areas_info) { - $_->{url} = $c->uri_for( '/reports/' . $c->cobrand->short_name( $_, $areas_info ) ); - if ($_->{parent_area} && $_->{url} =~ /,|%2C/) { - $_->{name} .= ', ' . $areas_info->{$_->{parent_area}}{name}; - } + if ( my $body = $c->cobrand->all_reports_single_body ) { + $c->stash->{body} = $body; + $c->detach( 'redirect_body' ); } - $c->stash->{areas_info} = $areas_info; - my @keys = sort { strcoll($areas_info->{$a}{name}, $areas_info->{$b}{name}) } keys %$areas_info; - @keys = $c->cobrand->filter_all_council_ids_list( @keys ); - $c->stash->{areas_info_sorted} = [ map { $areas_info->{$_} } @keys ]; + # Fetch all areas of the types we're interested in + my @bodies = $c->model('DB::Body')->all; + @bodies = sort { strcoll($a->name, $b->name) } @bodies; + $c->stash->{bodies} = \@bodies; eval { my $data = File::Slurp::read_file( @@ -99,97 +97,165 @@ Show the summary page for a particular ward. =cut sub ward : Path : Args(2) { - my ( $self, $c, $council, $ward ) = @_; + my ( $self, $c, $body, $ward ) = @_; - $c->forward( 'body_check', [ $council ] ); + $c->forward( 'body_check', [ $body ] ); $c->forward( 'ward_check', [ $ward ] ) if $ward; - $c->forward( 'load_parent' ); - $c->forward( 'check_canonical_url', [ $council ] ); + $c->forward( 'check_canonical_url', [ $body ] ); $c->forward( 'load_and_group_problems' ); - my $council_short = $c->cobrand->short_name( $c->stash->{council}, $c->stash->{areas_info} ); - $c->stash->{rss_url} = '/rss/reports/' . $council_short; + my $body_short = $c->cobrand->short_name( $c->stash->{body} ); + $c->stash->{rss_url} = '/rss/reports/' . $body_short; $c->stash->{rss_url} .= '/' . $c->cobrand->short_name( $c->stash->{ward} ) if $c->stash->{ward}; - $c->stash->{council_url} = '/reports/' . $council_short; + $c->stash->{body_url} = '/reports/' . $body_short; $c->stash->{stats} = $c->cobrand->get_report_stats(); my $pins = $c->stash->{pins}; $c->stash->{page} = 'reports'; # So the map knows to make clickable pins - FixMyStreet::Map::display_map( - $c, + my %map_params = ( latitude => @$pins ? $pins->[0]{latitude} : 0, longitude => @$pins ? $pins->[0]{longitude} : 0, - area => $c->stash->{ward} ? $c->stash->{ward}->{id} : $c->stash->{council}->{id}, - pins => $pins, + area => $c->stash->{ward} ? $c->stash->{ward}->{id} : [ keys %{$c->stash->{body}->areas} ], any_zoom => 1, ); + if ( $c->cobrand->moniker eq 'emptyhomes' ) { + FixMyStreet::Map::display_map( + $c, %map_params, latitude => 0, longitude => 0, + ); + } else { + FixMyStreet::Map::display_map( + $c, %map_params, pins => $pins, + ); + } $c->cobrand->tweak_all_reports_map( $c ); # List of wards - # Ignore external_body special council thing - unless ($c->stash->{ward} || !$c->stash->{council}->{id}) { - my $children = mySociety::MaPit::call('area/children', [ $c->stash->{council}->{id} ], + if ( !$c->stash->{ward} && $c->stash->{body}->id && $c->stash->{body}->body_areas->first ) { + my $children = mySociety::MaPit::call('area/children', [ $c->stash->{body}->body_areas->first->area_id ], type => $c->cobrand->area_types_children, ); - foreach (values %$children) { - $_->{url} = $c->uri_for( $c->stash->{council_url} - . '/' . $c->cobrand->short_name( $_ ) - ); + unless ($children->{error}) { + foreach (values %$children) { + $_->{url} = $c->uri_for( $c->stash->{body_url} + . '/' . $c->cobrand->short_name( $_ ) + ); + } + $c->stash->{children} = $children; } - $c->stash->{children} = $children; } } -sub rss_body : Regex('^rss/(reports|area)$') : Args(1) { - my ( $self, $c, $body ) = @_; - $c->detach( 'rss_ward', [ $body ] ); +sub rss_area : Path('/rss/area') : Args(1) { + my ( $self, $c, $area ) = @_; + $c->detach( 'rss_area_ward', [ $area ] ); } -sub rss_ward : Regex('^rss/(reports|area)$') : Args(2) { - my ( $self, $c, $council, $ward ) = @_; - - my ( $rss ) = $c->req->captures->[0]; +sub rss_area_ward : Path('/rss/area') : Args(2) { + my ( $self, $c, $area, $ward ) = @_; $c->stash->{rss} = 1; - $c->forward( 'body_check', [ $council ] ); - $c->forward( 'ward_check', [ $ward ] ) if $ward; + # area_check + + $area =~ s/\+/ /g; + $area =~ s/\.html//; - if ($rss eq 'area' && $c->stash->{council}{type} ne 'DIS' && $c->stash->{council}{type} ne 'CTY') { - # Two possibilites are the same for one-tier councils, so redirect one to the other - $c->detach( 'redirect_area' ); + # XXX Currently body/area overlaps here are a bit muddy. + # We're checking an area here, but this function is currently doing that. + return if $c->cobrand->reports_body_check( $c, $area ); + + # If we're passed an ID number (don't think this is used anywhere, it + # certainly shouldn't be), just look that up on mapit and redirect + if ($area =~ /^\d+$/) { + my $council = mySociety::MaPit::call('area', $area); + $c->detach( 'redirect_index') if $council->{error}; + $c->stash->{body} = $council; + $c->detach( 'redirect_body' ); + } + + # We must now have a string to check on mapit + my $areas = mySociety::MaPit::call( 'areas', $area, + type => $c->cobrand->area_types, + ); + + if (keys %$areas == 1) { + ($c->stash->{area}) = values %$areas; + } else { + foreach (keys %$areas) { + if (lc($areas->{$_}->{name}) eq lc($area) || $areas->{$_}->{name} =~ /^\Q$area\E (Borough|City|District|County) Council$/i) { + $c->stash->{area} = $areas->{$_}; + } + } } - my $url = $c->cobrand->short_name( $c->stash->{council} ); - $url .= '/' . $c->cobrand->short_name( $c->stash->{ward} ) if $c->stash->{ward}; + $c->detach( 'redirect_index' ) unless $c->stash->{area}; + + $c->forward( 'ward_check', [ $ward ] ) if $ward; + + my $url = $c->cobrand->short_name( $c->stash->{area} ); + $url .= '/' . $c->cobrand->short_name( $c->stash->{ward} ) if $c->stash->{ward}; $c->stash->{qs} = "/$url"; - if ( $rss eq 'area' && $c->stash->{ward} ) { + if ($c->stash->{area}{type} ne 'DIS' && $c->stash->{area}{type} ne 'CTY') { + # UK-specific types - two possibilites are the same for one-tier councils, so redirect one to the other + # With bodies, this should presumably redirect if only one body covers + # the area, and then it will need that body's name (rather than + # assuming as now it is the same as the area) + $c->stash->{body} = $c->stash->{area}; + $c->detach( 'redirect_body' ); + } + + $c->stash->{type} = 'area_problems'; + if ( $c->stash->{ward} ) { # All problems within a particular ward - $c->stash->{type} = 'area_problems'; $c->stash->{title_params} = { NAME => $c->stash->{ward}{name} }; $c->stash->{db_params} = [ $c->stash->{ward}->{id} ]; - } elsif ( $rss eq 'area' ) { - # Problems within a particular council - $c->stash->{type} = 'area_problems'; - $c->stash->{title_params} = { NAME => $c->stash->{council}{name} }; - $c->stash->{db_params} = [ $c->stash->{council}->{id} ]; - } elsif ($c->stash->{ward}) { + } else { + # Problems within a particular area + $c->stash->{title_params} = { NAME => $c->stash->{area}->{name} }; + $c->stash->{db_params} = [ $c->stash->{area}->{id} ]; + } + + # Send on to the RSS generation + $c->forward( '/rss/output' ); + +} + +sub rss_body : Path('/rss/reports') : Args(1) { + my ( $self, $c, $body ) = @_; + $c->detach( 'rss_ward', [ $body ] ); +} + +sub rss_ward : Path('/rss/reports') : Args(2) { + my ( $self, $c, $body, $ward ) = @_; + + $c->stash->{rss} = 1; + + $c->forward( 'body_check', [ $body ] ); + $c->forward( 'ward_check', [ $ward ] ) if $ward; + + my $url = $c->cobrand->short_name( $c->stash->{body} ); + $url .= '/' . $c->cobrand->short_name( $c->stash->{ward} ) if $c->stash->{ward}; + $c->stash->{qs} = "/$url"; + + if ($c->stash->{ward}) { # Problems sent to a council, restricted to a ward $c->stash->{type} = 'ward_problems'; - $c->stash->{title_params} = { COUNCIL => $c->stash->{council}{name}, WARD => $c->stash->{ward}{name} }; - $c->stash->{db_params} = [ $c->stash->{council}->{id}, $c->stash->{ward}->{id} ]; + $c->stash->{title_params} = { COUNCIL => $c->stash->{body}->name, WARD => $c->stash->{ward}{name} }; + $c->stash->{db_params} = [ $c->stash->{body}->id, $c->stash->{ward}->{id} ]; } else { # Problems sent to a council $c->stash->{type} = 'council_problems'; - $c->stash->{title_params} = { COUNCIL => $c->stash->{council}{name} }; - $c->stash->{db_params} = [ $c->stash->{council}->{id}, $c->stash->{council}->{id} ]; + $c->stash->{title_params} = { COUNCIL => $c->stash->{body}->name }; + # XXX This looks up in both bodies_str and areas, but is only using body ID. + # This will not work properly in any install where body IDs are not === area IDs. + $c->stash->{db_params} = [ $c->stash->{body}->id, $c->stash->{body}->id ]; } # Send on to the RSS generation @@ -198,60 +264,47 @@ sub rss_ward : Regex('^rss/(reports|area)$') : Args(2) { =head2 body_check -This action checks the council or external_body name (or code) given in a URI -exists, is valid and so on. If it is, it stores the area or body in the stash, -otherwise it redirects to the all reports page. +This action checks the body name (or code) given in a URI exists, is valid and +so on. If it is, it stores the body in the stash, otherwise it redirects to the +all reports page. =cut sub body_check : Private { - my ( $self, $c, $q_council ) = @_; + my ( $self, $c, $q_body ) = @_; - $q_council =~ s/\+/ /g; - $q_council =~ s/\.html//; + $q_body =~ s/\+/ /g; + $q_body =~ s/\.html//; # Check cobrand specific incantations - e.g. ONS codes for UK, # Oslo/ kommunes sharing a name in Norway - return if $c->cobrand->reports_body_check( $c, $q_council ); + return if $c->cobrand->reports_body_check( $c, $q_body ); # If we're passed an ID number (don't think this is used anywhere, it # certainly shouldn't be), just look that up on MaPit and redirect - if ($q_council =~ /^\d+$/) { - my $council = mySociety::MaPit::call('area', $q_council); - $c->detach( 'redirect_index') if $council->{error}; - $c->stash->{council} = $council; - $c->detach( 'redirect_area' ); - } - - if ( $c->cobrand->reports_by_body ) { - my $problem = $c->cobrand->problems->search({ 'lower(me.external_body)' => lc $q_council }, { columns => [ 'external_body' ], rows => 1 })->single; - if ( $problem ) { - # If external_body, put as a council with ID 0 for the moment. - $c->stash->{council} = { id => 0, name => $problem->external_body }; - return; - } + if ($q_body =~ /^\d+$/) { + my $area = mySociety::MaPit::call('area', $q_body); + $c->detach( 'redirect_index') if $area->{error}; + $c->stash->{body} = $area; + $c->detach( 'redirect_body' ); } # We must now have a string to check - my $area_types = $c->cobrand->area_types; - my $areas = mySociety::MaPit::call( 'areas', $q_council, - type => $area_types, - min_generation => $c->cobrand->area_min_generation - ); + my @bodies = $c->model('DB::Body')->search( { name => { -like => "$q_body%" } } )->all; - if (keys %$areas == 1) { - ($c->stash->{council}) = values %$areas; + if (@bodies == 1) { + $c->stash->{body} = $bodies[0]; return; } else { - foreach (keys %$areas) { - if (lc($areas->{$_}->{name}) eq lc($q_council) || $areas->{$_}->{name} =~ /^\Q$q_council\E (Borough|City|District|County) Council$/i) { - $c->stash->{council} = $areas->{$_}; + foreach (@bodies) { + if (lc($_->name) eq lc($q_body) || $_->name =~ /^\Q$q_body\E (Borough|City|District|County) Council$/i) { + $c->stash->{body} = $_; return; } } } - # No result, bad council name. + # No result, bad body name. $c->detach( 'redirect_index' ); } @@ -259,7 +312,7 @@ sub body_check : Private { This action checks the ward name from a URI exists and is part of the right parent, already found with body_check. It either stores the ward Area if -okay, or redirects to the council page if bad. +okay, or redirects to the body page if bad. =cut @@ -270,48 +323,45 @@ sub ward_check : Private { $ward =~ s/\.html//; $ward =~ s{_}{/}g; - my $council = $c->stash->{council}; + # Could be from RSS area, or body... + my $parent_id; + if ( $c->stash->{body} ) { + $parent_id = $c->stash->{body}->body_areas->first; + $c->detach( 'redirect_body' ) unless $parent_id; + $parent_id = $parent_id->area_id; + } else { + $parent_id = $c->stash->{area}->{id}; + } my $qw = mySociety::MaPit::call('areas', $ward, type => $c->cobrand->area_types_children, - min_generation => $c->cobrand->area_min_generation ); foreach my $area (sort { $a->{name} cmp $b->{name} } values %$qw) { - if ($area->{parent_area} == $council->{id}) { + if ($area->{parent_area} == $parent_id) { $c->stash->{ward} = $area; return; } } # Given a false ward name - $c->detach( 'redirect_area' ); -} - -sub load_parent : Private { - my ( $self, $c ) = @_; - - my $council = $c->stash->{council}; - my $areas_info; - if ($council->{parent_area}) { - $c->stash->{areas_info} = mySociety::MaPit::call('areas', [ $council->{id}, $council->{parent_area} ]) - } else { - $c->stash->{areas_info} = { $council->{id} => $council }; - } + $c->stash->{body} = $c->stash->{area} + unless $c->stash->{body}; + $c->detach( 'redirect_body' ); } =head2 check_canonical_url -Given an already found (case-insensitively) council, check what URL +Given an already found (case-insensitively) body, check what URL we are at and redirect accordingly if different. =cut sub check_canonical_url : Private { - my ( $self, $c, $q_council ) = @_; + my ( $self, $c, $q_body ) = @_; - my $council_short = $c->cobrand->short_name( $c->stash->{council}, $c->stash->{areas_info} ); - my $url_short = URI::Escape::uri_escape_utf8($q_council); + my $body_short = $c->cobrand->short_name( $c->stash->{body} ); + my $url_short = URI::Escape::uri_escape_utf8($q_body); $url_short =~ s/%2B/+/g; - $c->detach( 'redirect_area' ) unless $council_short eq $url_short; + $c->detach( 'redirect_body' ) unless $body_short eq $url_short; } sub load_and_group_problems : Private { @@ -325,69 +375,54 @@ sub load_and_group_problems : Private { }; if ($c->stash->{ward}) { $where->{areas} = { 'like', '%,' . $c->stash->{ward}->{id} . ',%' }; - $where->{council} = [ + $where->{bodies_str} = [ undef, - $c->stash->{council}->{id}, - { 'like', $c->stash->{council}->{id} . ',%' }, - { 'like', '%,' . $c->stash->{council}->{id} }, + $c->stash->{body}->id, + { 'like', $c->stash->{body}->id . ',%' }, + { 'like', '%,' . $c->stash->{body}->id }, ]; - } elsif ($c->stash->{council} && $c->stash->{council}->{id} == 0) { - # A proxy for an external_body - $where->{'lower(external_body)'} = lc $c->stash->{council}->{name}; - } elsif ($c->stash->{council}) { - $where->{areas} = { 'like', '%,' . $c->stash->{council}->{id} . ',%' }; - $where->{council} = [ - undef, - $c->stash->{council}->{id}, - { 'like', $c->stash->{council}->{id} . ',%' }, - { 'like', '%,' . $c->stash->{council}->{id} }, + } elsif ($c->stash->{body}) { + # XXX FixMyStreet used to have the following line so that reports not + # currently sent anywhere could still be listed in the appropriate + # (body/area), as they were the same. Now they're not, not sure if + # there's a way to do this easily. + #$where->{areas} = { 'like', '%,' . $c->stash->{body}->id . ',%' }; + $where->{bodies_str} = [ + # undef, + $c->stash->{body}->id, + { 'like', $c->stash->{body}->id . ',%' }, + { 'like', '%,' . $c->stash->{body}->id }, ]; } my $problems = $c->cobrand->problems->search( $where, { - columns => [ - 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', 'cobrand', - #{ duration => { extract => "epoch from current_timestamp-lastupdate" } }, - #{ age => { extract => "epoch from current_timestamp-confirmed" } }, - { confirmed => { extract => 'epoch from confirmed' } }, - { whensent => { extract => 'epoch from whensent' } }, - { lastupdate => { extract => 'epoch from lastupdate' } }, - { photo => 'photo is not null' }, - ], order_by => { -desc => 'lastupdate' }, rows => $c->cobrand->reports_per_page, } )->page( $page ); $c->stash->{pager} = $problems->pager; - $problems = $problems->cursor; # Raw DB cursor for speed my ( %problems, @pins ); - my $re_councils = join('|', keys %{$c->stash->{areas_info}}); - my @cols = ( 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', 'cobrand', 'confirmed', 'whensent', 'lastupdate', 'photo' ); - while ( my @problem = $problems->next ) { - my %problem = zip @cols, @problem; - $problem{is_fixed} = FixMyStreet::DB::Result::Problem->fixed_states()->{$problem{state}}; - $c->log->debug( $problem{'cobrand'} . ', cobrand is ' . $c->cobrand->moniker ); - if ( !$c->stash->{council}->{id} ) { - # An external_body entry - add_row( \%problem, 0, \%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; } - if ( !$problem{council} ) { - # Problem was not sent to any council, add to possible councils - $problem{councils} = 0; - while ($problem{areas} =~ /,($re_councils)(?=,)/g) { - add_row( \%problem, $1, \%problems, \@pins ); + if ( !$problem->bodies_str ) { + # Problem was not sent to any body, add to all possible areas XXX + my $a = $problem->areas; # Store, as otherwise is looked up every iteration. + while ($a =~ /,(\d+)(?=,)/g) { + add_row( $c, $problem, $1, \%problems, \@pins ); } } else { - # Add to councils it was sent to - (my $council = $problem{council}) =~ s/\|.*$//; - my @council = split( /,/, $council ); - $problem{councils} = scalar @council; - foreach ( @council ) { - next if $_ != $c->stash->{council}->{id}; - add_row( \%problem, $_, \%problems, \@pins ); + # Add to bodies it was sent to + # XXX Assumes body ID matches "council ID" + my $bodies = $problem->bodies_str_ids; + foreach ( @$bodies ) { + next if $_ != $c->stash->{body}->id; + add_row( $c, $problem, $_, \%problems, \@pins ); } } } @@ -406,26 +441,26 @@ sub redirect_index : Private { $c->res->redirect( $c->uri_for($url) ); } -sub redirect_area : Private { +sub redirect_body : Private { my ( $self, $c ) = @_; my $url = ''; $url .= "/rss" if $c->stash->{rss}; $url .= '/reports'; - $url .= '/' . $c->cobrand->short_name( $c->stash->{council}, $c->stash->{areas_info} ); + $url .= '/' . $c->cobrand->short_name( $c->stash->{body} ); $url .= '/' . $c->cobrand->short_name( $c->stash->{ward} ) if $c->stash->{ward}; $c->res->redirect( $c->uri_for($url) ); } sub add_row { - my ( $problem, $council, $problems, $pins ) = @_; - push @{$problems->{$council}}, $problem; + my ( $c, $problem, $body, $problems, $pins ) = @_; + push @{$problems->{$body}}, $problem; push @$pins, { - latitude => $problem->{latitude}, - longitude => $problem->{longitude}, - colour => 'yellow', # FixMyStreet::DB::Result::Problem->fixed_states()->{$problem->{state}} ? 'green' : 'red', - id => $problem->{id}, - title => $problem->{title}, + latitude => $problem->latitude, + longitude => $problem->longitude, + colour => $c->cobrand->pin_colour( $problem, 'reports' ), + id => $problem->id, + title => $problem->title_safe, }; } diff --git a/perllib/FixMyStreet/App/Controller/Rss.pm b/perllib/FixMyStreet/App/Controller/Rss.pm index baaa3b927..ed47f6f87 100755 --- a/perllib/FixMyStreet/App/Controller/Rss.pm +++ b/perllib/FixMyStreet/App/Controller/Rss.pm @@ -241,6 +241,15 @@ sub add_row : Private { $row->{name} = 'anonymous' if $row->{anonymous} || !$row->{name}; my $pubDate; + if ($row->{created}) { + $row->{created} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/; + $pubDate = mySociety::Locale::in_gb_locale { + strftime("%a, %d %b %Y %H:%M:%S %z", $6, $5, $4, $3, $2-1, $1-1900, -1, -1, 0) + }; + $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'; + } if ($row->{confirmed}) { $row->{confirmed} =~ /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/; $pubDate = mySociety::Locale::in_gb_locale { @@ -257,7 +266,7 @@ sub add_row : Private { my $hashref_restriction = $c->cobrand->site_restriction; my $base_url = $c->cobrand->base_url; - if ( $hashref_restriction && $hashref_restriction->{council} && $row->{council} && $row->{council} ne $hashref_restriction->{council} ) { + if ( $hashref_restriction && $hashref_restriction->{bodies_str} && $row->{bodies_str} && $row->{bodies_str} ne $hashref_restriction->{bodies_str} ) { $base_url = $c->config->{BASE_URL}; } my $url = $base_url . $link; @@ -271,7 +280,7 @@ sub add_row : Private { $item{pubDate} = $pubDate if $pubDate; $item{category} = $row->{category} if $row->{category}; - if ($c->cobrand->allow_photo_display && $row->{photo}) { + if ($c->cobrand->allow_photo_display($row) && $row->{photo}) { my $key = $alert_type->item_table eq 'comment' ? 'c/' : ''; $item{description} .= ent("\n<br><img src=\"". $base_url . "/photo/$key$row->{id}.jpeg\">"); } diff --git a/perllib/FixMyStreet/App/Controller/Static.pm b/perllib/FixMyStreet/App/Controller/Static.pm index 723f0f2e1..40e2431ea 100755 --- a/perllib/FixMyStreet/App/Controller/Static.pm +++ b/perllib/FixMyStreet/App/Controller/Static.pm @@ -19,7 +19,10 @@ template depending on language, will need extending at some point. sub about : Global : Args(0) { my ( $self, $c ) = @_; - # don't need to do anything here - should just pass through. + + my $lang_code = $c->stash->{lang_code}; + my $template = "static/about-$lang_code.html"; + $c->stash->{template} = $template; } sub privacy : Global : Args(0) { @@ -54,6 +57,10 @@ sub iphone : Global : Args(0) { my ( $self, $c ) = @_; } +sub council : Global : Args(0) { + my ( $self, $c ) = @_; +} + __PACKAGE__->meta->make_immutable; 1; diff --git a/perllib/FixMyStreet/App/Controller/Tokens.pm b/perllib/FixMyStreet/App/Controller/Tokens.pm index 03dc69b00..44cb2429d 100644 --- a/perllib/FixMyStreet/App/Controller/Tokens.pm +++ b/perllib/FixMyStreet/App/Controller/Tokens.pm @@ -53,6 +53,25 @@ sub confirm_problem : Path('/P') { return; } + # For Zurich, email confirmation simply sets a flag, it does not change the + # problem state, log in, or anything else + if ($c->cobrand->moniker eq 'zurich') { + my $extra = { %{ $problem->extra || {} } }; + $extra->{email_confirmed} = 1; + $problem->update( { + extra => $extra, + confirmed => \'ms_current_timestamp()', + } ); + + if ( ref($data) && ( $data->{name} || $data->{password} ) ) { + $problem->user->name( $data->{name} ) if $data->{name}; + $problem->user->phone( $data->{phone} ) if $data->{phone}; + $problem->user->update; + } + + return 1; + } + # We have a problem - confirm it if needed! my $old_state = $problem->state; $problem->update( @@ -83,6 +102,7 @@ sub confirm_problem : Path('/P') { $c->res->redirect($report_uri); } + $c->stash->{created_report} = 'fromemail'; return 1; } @@ -176,7 +196,12 @@ sub confirm_update : Path('/C') { $c->authenticate( { email => $comment->user->email }, 'no_password' ); $c->set_session_cookie_expire(0); - $c->forward('/report/update/confirm'); + if ( $comment->confirmed ) { + my $report_uri = $c->cobrand->base_url_for_report( $comment->problem ) . $comment->problem->url; + $c->res->redirect($report_uri); + } else { + $c->forward('/report/update/confirm'); + } return 1; } diff --git a/perllib/FixMyStreet/App/View/Web.pm b/perllib/FixMyStreet/App/View/Web.pm index 42878be37..e4aafe951 100644 --- a/perllib/FixMyStreet/App/View/Web.pm +++ b/perllib/FixMyStreet/App/View/Web.pm @@ -18,8 +18,8 @@ __PACKAGE__->config( ENCODING => 'utf8', render_die => 1, expose_methods => [ - 'loc', 'nget', 'tprintf', 'display_crosssell_advert', 'prettify_epoch', - 'add_links', 'version', + 'loc', 'nget', 'tprintf', 'display_crosssell_advert', 'prettify_dt', + 'add_links', 'version', 'decode', ], FILTERS => { escape_js => \&escape_js, @@ -92,20 +92,20 @@ sub display_crosssell_advert { return CrossSell::display_advert( $c, $email, $name, %data ); } -=head2 Utils::prettify_epoch +=head2 Utils::prettify_dt - [% pretty = prettify_epoch( $epoch, $short_bool ) %] + [% pretty = prettify_dt( $dt, $short_bool ) %] -Return a pretty version of the epoch. +Return a pretty version of the DateTime object. $short_bool = 1; # 16:02, 29 Mar 2011 $short_bool = 0; # 16:02, Tuesday 29 March 2011 =cut -sub prettify_epoch { +sub prettify_dt { my ( $self, $c, $epoch, $short_bool ) = @_; - return Utils::prettify_epoch( $epoch, $short_bool ); + return Utils::prettify_dt( $epoch, $short_bool ); } =head2 add_links @@ -121,10 +121,16 @@ sub add_links { $text =~ s/\r//g; $text = ent($text); - $text =~ s{(https?://[^\s]+)}{<a href="$1">$1</a>}g; + $text =~ s{(https?://)([^\s]+)}{"<a href='$1$2'>$1" . _space_slash($2) . '</a>'}ge; return $text; } +sub _space_slash { + my $t = shift; + $t =~ s{/(?!$)}{/ }g; + return $t; +} + =head2 escape_js Used to escape strings that are going to be put inside JavaScript. @@ -175,5 +181,11 @@ sub version { return "$file?$version_hash{$file}"; } +sub decode { + my ( $self, $c, $text ) = @_; + utf8::decode($text) unless utf8::is_utf8($text); + return $text; +} + 1; diff --git a/perllib/FixMyStreet/Cobrand.pm b/perllib/FixMyStreet/Cobrand.pm index 881183463..ff7d7f943 100644 --- a/perllib/FixMyStreet/Cobrand.pm +++ b/perllib/FixMyStreet/Cobrand.pm @@ -8,6 +8,7 @@ use warnings; use FixMyStreet; use Carp; +use Moose; use Module::Pluggable sub_name => '_cobrands', @@ -38,7 +39,10 @@ Simply returns the config variable (so this function can be overridden in test s =cut sub _get_allowed_cobrands { - return FixMyStreet->config('ALLOWED_COBRANDS') || []; + my $allowed = FixMyStreet->config('ALLOWED_COBRANDS') || []; + # If the user has supplied a string, convert to an arrayref + $allowed = [ $allowed ] unless ref $allowed; + return $allowed; } =head2 available_cobrand_classes @@ -92,7 +96,14 @@ sub get_class_for_host { my $class = shift; my $host = shift; - foreach my $avail ( $class->available_cobrand_classes ) { + my @available = $class->available_cobrand_classes; + + # If only one entry, always use it + return class($available[0]) if 1 == @available; + + # If more than one entry, pick first whose regex (or + # name by default) matches hostname + foreach my $avail ( @available ) { return class($avail) if $host =~ /$avail->{host}/; } diff --git a/perllib/FixMyStreet/Cobrand/BellaVistaEnAccion.pm b/perllib/FixMyStreet/Cobrand/BellaVistaEnAccion.pm new file mode 100644 index 000000000..d96e7bc96 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/BellaVistaEnAccion.pm @@ -0,0 +1,33 @@ +package FixMyStreet::Cobrand::BellaVistaEnAccion; +use base 'FixMyStreet::Cobrand::Default'; + +use strict; +use warnings; + +sub path_to_web_templates { + my $self = shift; + return [ + FixMyStreet->path_to( 'templates/web', $self->moniker )->stringify, + FixMyStreet->path_to( 'templates/web/fixmystreet' )->stringify + ]; +} + +sub country { + return 'CL'; +} + +sub example_places { + return ( 'Dominica, Recoleta', 'Pio Nono' ); +} + +sub languages { [ 'es-cl,Castellano,es_CL', 'en-gb,English,en_GB' ] } + +sub disambiguate_location { + return { + country => 'cl', + town => 'Santiago', + }; +} + +1; + diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm index c33135673..f648225ae 100644 --- a/perllib/FixMyStreet/Cobrand/Bromley.pm +++ b/perllib/FixMyStreet/Cobrand/Bromley.pm @@ -29,9 +29,8 @@ sub disambiguate_location { my $town = 'Bromley'; # Bing turns High St Bromley into Bromley High St which is in # Bromley by Bow. - if ( $string =~ /high\+st/i ) { - $town .= ', BR1'; - } + $town .= ', BR1' if $string =~ /^high\s+st(reet)?$/i; + $town = '' if $string =~ /orpington/i; return { %{ $self->SUPER::disambiguate_location() }, town => $town, @@ -80,9 +79,7 @@ sub process_extras { sub contact_email { my $self = shift; - my $type = shift || ''; - return join( '@', 'info', 'bromley.gov.uk' ) if $type eq 'contact'; - return $self->next::method(); + return join( '@', 'info', 'bromley.gov.uk' ); } sub contact_name { 'Bromley Council (do not reply)'; } @@ -100,6 +97,9 @@ sub tweak_all_reports_map { } } +sub title_list { + return ["MR", "MISS", "MRS", "MS", "DR"]; +} 1; diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 34f9d0b1d..c8bae90e6 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::Geocode::Bing; use Encode; use URI; use Digest::MD5 qw(md5_hex); @@ -294,8 +295,11 @@ to null/0. sub uri { my ( $self, $uri ) = @_; - (my $map_class = $FixMyStreet::Map::map_class) =~ s/^FixMyStreet::Map:://; - return $uri unless $map_class =~ /OSM|FMS/; + { + no warnings 'once'; + (my $map_class = $FixMyStreet::Map::map_class) =~ s/^FixMyStreet::Map:://; + return $uri unless $map_class =~ /OSM|FMS/; + } $uri->query_param( zoom => 3 ) if $uri->query_param('lat') && !$uri->query_param('zoom'); @@ -363,7 +367,10 @@ Return a boolean indicating whether the cobrand allows photo display =cut -sub allow_photo_display { return 1; } +sub allow_photo_display { + my ( $self, $r ) = @_; + return 1; +} =head2 allow_update_reporting @@ -475,23 +482,23 @@ sub format_postcode { return $postcode; } -=head2 council_check +=head2 area_check -Paramters are COUNCILS, QUERY, CONTEXT. Return a boolean indicating whether -COUNCILS pass any extra checks. CONTEXT is where we are on the site. +Paramters are AREAS, QUERY, CONTEXT. Return a boolean indicating whether +AREAS pass any extra checks. CONTEXT is where we are on the site. =cut -sub council_check { return ( 1, '' ); } +sub area_check { return ( 1, '' ); } -=head2 all_councils_report +=head2 all_reports_single_body Return a boolean indicating whether the cobrand displays a report of all councils =cut -sub all_councils_report { 1 } +sub all_reports_single_body { 0 } =head2 ask_ever_reported @@ -525,7 +532,7 @@ Show the problem creation graph in the admin interface sub admin_show_creation_graph { 1 } -=head2 area_types, area_min_generation +=head2 area_types The MaPit types this site handles @@ -533,7 +540,6 @@ The MaPit types this site handles sub area_types { FixMyStreet->config('MAPIT_TYPES') || [ 'ZZZ' ] } sub area_types_children { FixMyStreet->config('MAPIT_TYPES_CHILDREN') || [] } -sub area_min_generation { '' } =head2 contact_name, contact_email @@ -555,39 +561,36 @@ sub email_host { return 1; } -=item remove_redundant_councils +=item remove_redundant_areas -Remove councils whose reports go to another council +Remove areas whose reports go to another area (XXX) =cut -sub remove_redundant_councils { - my $self = shift; - my $all_councils = shift; -} - -=item filter_all_council_ids_list - -Removes any council IDs that we don't need from an array and returns the -filtered array +sub remove_redundant_areas { + my $self = shift; + my $all_areas = shift; -=cut + my $whitelist = FixMyStreet->config('MAPIT_ID_WHITELIST'); + return unless $whitelist && ref $whitelist eq 'ARRAY' && @$whitelist; -sub filter_all_council_ids_list { - my $self = shift; - return @_; + my %whitelist = map { $_ => 1 } @$whitelist; + foreach (keys %$all_areas) { + delete $all_areas->{$_} unless $whitelist{$_}; + } } =item short_name -Remove extra information from council names for tidy URIs +Remove extra information from body names for tidy URIs =cut sub short_name { my $self = shift; - my ($area, $info) = @_; - my $name = $area->{name}; + my ($area) = @_; + + my $name = $area->{name} || $area->name; $name = URI::Escape::uri_escape_utf8($name); $name =~ s/%20/+/g; return $name; @@ -600,6 +603,13 @@ For UK sub-cobrands, to specify various alternations needed for them. =cut sub is_council { 0; } +=item is_two_tier + +For UK sub-cobrands, to specify various alternations needed for them. + +=cut +sub is_two_tier { 0; } + =item council_rss_alert_options Generate a set of options for council rss alerts. @@ -607,10 +617,10 @@ Generate a set of options for council rss alerts. =cut sub council_rss_alert_options { - my ( $self, $all_councils, $c ) = @_; + my ( $self, $all_areas, $c ) = @_; my ( @options, @reported_to_options ); - foreach (values %$all_councils) { + foreach (values %$all_areas) { $_->{short_name} = $self->short_name( $_ ); ( $_->{id_name} = $_->{short_name} ) =~ tr/+/_/; push @options, { @@ -654,31 +664,26 @@ Get stats to display on the council reports page sub get_report_stats { return 0; } -sub get_council_sender { - my ( $self, $area_id, $area_info, $category ) = @_; - - my $send_method; - - my $council_config = FixMyStreet::App->model("DB::Open311conf")->search( { area_id => $area_id } )->first; - $send_method = $council_config->send_method if $council_config; +sub get_body_sender { + my ( $self, $body, $category ) = @_; - if ( $council_config && $council_config->can_be_devolved ) { + if ( $body->can_be_devolved ) { # look up via category - my $config = FixMyStreet::App->model("DB::Contact")->search( { area_id => $area_id, category => $category } )->first; + my $config = FixMyStreet::App->model("DB::Contact")->search( { body_id => $body->id, category => $category } )->first; if ( $config->send_method ) { return { method => $config->send_method, config => $config }; } else { - return { method => $send_method, config => $council_config }; + return { method => $body->send_method, config => $body }; } - } elsif ( $send_method ) { - return { method => $send_method, config => $council_config }; + } elsif ( $body->send_method ) { + return { method => $body->send_method, config => $body }; } - return $self->_fallback_council_sender( $area_id, $area_info, $category ); + return $self->_fallback_body_sender( $body, $category ); } -sub _fallback_council_sender { - my ( $self, $area_id, $area_info, $category ) = @_; +sub _fallback_body_sender { + my ( $self, $body, $category ) = @_; return { method => 'Email' }; }; @@ -689,9 +694,17 @@ sub example_places { return $e; } +=head2 title_list + +Returns an arrayref of possible titles for a person to send to the mobile app. + +=cut + +sub title_list { return undef; } + =head2 only_authed_can_create -If true, only users with the from_council flag set are able to create reports. +If true, only users with the from_body flag set are able to create reports. =cut @@ -717,11 +730,23 @@ Returns the colour of pin to be used for a particular report =cut sub pin_colour { my ( $self, $p, $context ) = @_; - #return 'green' if time() - $p->confirmed_local->epoch < 7 * 24 * 60 * 60; - return 'yellow' if $context eq 'around'; + #return 'green' if time() - $p->confirmed->epoch < 7 * 24 * 60 * 60; + return 'yellow' if $context eq 'around' || $context eq 'reports' || $context eq 'report'; return $p->is_fixed ? 'green' : 'red'; } +=head2 path_to_pin_icons + +Used to override the path for the pin icons if you want to add custom pin icons +for your cobrand. + +=cut + +sub path_to_pin_icons { + return '/i/'; +} + + =head2 tweak_all_reports_map Used to tweak the display settings of the map on the all reports pages. @@ -738,14 +763,15 @@ sub default_map_zoom { undef }; sub users_can_hide { return 0; } -=head2 reports_by_body +=head2 default_show_name -Can /reports show reports indexed by external_body? This is a temporary measure -until the contacts/area/body handling is rewritten to be better. +Returns true if the show name checkbox should be ticked by default. =cut -sub reports_by_body { 0; } +sub default_show_name { + 1; +} =head2 report_check_for_errors @@ -792,5 +818,65 @@ a name key sub anonymous_account { undef; } +=head2 show_unconfirmed_reports + +Whether reports in state 'unconfirmed' should still be shown on the public site. +(They're always included in the admin interface.) + +=cut + +sub show_unconfirmed_reports { + 0; +} + +=head2 never_confirm_updates + +If true then we never send an email to confirm an update + +=cut + +sub never_confirm_updates { 0; } + +sub include_time_in_update_alerts { 0; } + +=head2 prettify_dt + + my $date = $c->prettify_dt( $datetime ); + +Takes a datetime object and returns a string representation. + +=cut + +sub prettify_dt { + my $self = shift; + my $dt = shift; + + return Utils::prettify_dt( $dt, 1 ); +} + +=head2 extra_contact_validation + +Perform any extra validation on the contact form. + +=cut + +sub extra_contact_validation { (); } + +sub problem_as_hashref { + my $self = shift; + my $problem = shift; + my $ctx = shift; + + return $problem->as_hashref( $ctx ); +} + +sub updates_as_hashref { + my $self = shift; + my $problem = shift; + my $ctx = shift; + + return {}; +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/EmptyHomes.pm b/perllib/FixMyStreet/Cobrand/EmptyHomes.pm index c3d13448d..47ea023d9 100644 --- a/perllib/FixMyStreet/Cobrand/EmptyHomes.pm +++ b/perllib/FixMyStreet/Cobrand/EmptyHomes.pm @@ -62,6 +62,20 @@ to be resized then return 0; sub default_photo_resize { return '195x'; } +sub short_name { + my $self = shift; + my ($area) = @_; + + my $name = $area->{name} || $area->name; + $name =~ s/ (Borough|City|District|County) Council$//; + $name =~ s/ Council$//; + $name =~ s/ & / and /; + $name =~ s{/}{_}g; + $name = URI::Escape::uri_escape_utf8($name); + $name =~ s/%20/-/g; + return $name; +} + =item council_rss_alert_options Generate a set of options for council rss alerts. @@ -107,5 +121,426 @@ sub council_rss_alert_options { return ( \@options, @reported_to_options ? \@reported_to_options : undef ); } +sub process_extras { + my $self = shift; + my $ctx = shift; + my $body_id = shift; + my $extra = shift; + + my $value = $ctx->request->params->{address} || ''; + $ctx->stash->{field_errors}->{address} = _('This information is required') + unless $value; + $extra->{address} = $value; +} + +sub front_stats_data { + my ( $self ) = @_; + my $key = "recent_new"; + my $result = Memcached::get($key); + unless ($result) { + $result = $self->problems->search( + { state => [ FixMyStreet::DB::Result::Problem->visible_states() ] } + )->count; + foreach my $v (values %{$self->old_site_stats}) { + $result += $v; + } + Memcached::set($key, $result, 3600); + } + return $result; +} + +# A record of the number of reports from the Channel 4 site and other old data +sub old_site_stats { + return { + 2223 => 95, + 2238 => 82, + 2245 => 54, + 2248 => 31, + 2250 => 132, + 2253 => 15, + 2255 => 25, + 2256 => 8, + 2257 => 3, + 2258 => 14, + 2259 => 5, + 2260 => 22, + 2261 => 12, + 2262 => 21, + 2263 => 14, + 2264 => 1, + 2267 => 1, + 2271 => 13, + 2272 => 7, + 2273 => 13, + 2274 => 7, + 2275 => 15, + 2276 => 14, + 2277 => 10, + 2278 => 7, + 2279 => 23, + 2280 => 16, + 2281 => 25, + 2282 => 14, + 2283 => 10, + 2284 => 22, + 2285 => 25, + 2286 => 32, + 2287 => 13, + 2288 => 13, + 2289 => 16, + 2290 => 18, + 2291 => 1, + 2292 => 9, + 2293 => 15, + 2294 => 16, + 2295 => 12, + 2296 => 4, + 2299 => 2, + 2300 => 1, + 2304 => 10, + 2305 => 17, + 2306 => 6, + 2307 => 11, + 2308 => 17, + 2309 => 9, + 2310 => 6, + 2311 => 9, + 2312 => 26, + 2313 => 2, + 2314 => 34, + 2315 => 18, + 2316 => 13, + 2317 => 17, + 2318 => 7, + 2319 => 14, + 2320 => 4, + 2321 => 20, + 2322 => 7, + 2323 => 10, + 2324 => 7, + 2325 => 15, + 2326 => 12, + 2327 => 25, + 2328 => 23, + 2329 => 11, + 2330 => 4, + 2331 => 29, + 2332 => 12, + 2333 => 7, + 2334 => 5, + 2335 => 16, + 2336 => 7, + 2337 => 7, + 2338 => 2, + 2339 => 12, + 2340 => 2, + 2341 => 7, + 2342 => 14, + 2343 => 20, + 2344 => 13, + 2345 => 17, + 2346 => 6, + 2347 => 4, + 2348 => 6, + 2349 => 18, + 2350 => 13, + 2351 => 11, + 2352 => 24, + 2353 => 10, + 2354 => 20, + 2355 => 14, + 2356 => 13, + 2357 => 14, + 2358 => 8, + 2359 => 6, + 2360 => 10, + 2361 => 36, + 2362 => 17, + 2363 => 8, + 2364 => 7, + 2365 => 8, + 2366 => 26, + 2367 => 19, + 2368 => 20, + 2369 => 8, + 2370 => 14, + 2371 => 79, + 2372 => 10, + 2373 => 5, + 2374 => 4, + 2375 => 12, + 2376 => 10, + 2377 => 24, + 2378 => 9, + 2379 => 8, + 2380 => 25, + 2381 => 13, + 2382 => 11, + 2383 => 16, + 2384 => 18, + 2385 => 12, + 2386 => 18, + 2387 => 5, + 2388 => 8, + 2389 => 12, + 2390 => 11, + 2391 => 23, + 2392 => 11, + 2393 => 16, + 2394 => 9, + 2395 => 27, + 2396 => 8, + 2397 => 27, + 2398 => 14, + 2402 => 1, + 2403 => 18, + 2404 => 14, + 2405 => 7, + 2406 => 9, + 2407 => 12, + 2408 => 3, + 2409 => 8, + 2410 => 23, + 2411 => 27, + 2412 => 9, + 2413 => 20, + 2414 => 96, + 2415 => 11, + 2416 => 20, + 2417 => 18, + 2418 => 24, + 2419 => 18, + 2420 => 7, + 2421 => 29, + 2427 => 7, + 2428 => 15, + 2429 => 18, + 2430 => 32, + 2431 => 9, + 2432 => 17, + 2433 => 8, + 2434 => 10, + 2435 => 14, + 2436 => 13, + 2437 => 11, + 2438 => 5, + 2439 => 4, + 2440 => 23, + 2441 => 8, + 2442 => 18, + 2443 => 12, + 2444 => 3, + 2445 => 8, + 2446 => 31, + 2447 => 15, + 2448 => 3, + 2449 => 12, + 2450 => 11, + 2451 => 8, + 2452 => 20, + 2453 => 25, + 2454 => 8, + 2455 => 6, + 2456 => 24, + 2457 => 6, + 2458 => 10, + 2459 => 15, + 2460 => 17, + 2461 => 20, + 2462 => 12, + 2463 => 16, + 2464 => 5, + 2465 => 14, + 2466 => 20, + 2467 => 14, + 2468 => 12, + 2469 => 4, + 2470 => 1, + 2471 => 1, + 2474 => 9, + 2475 => 12, + 2476 => 11, + 2477 => 9, + 2478 => 10, + 2479 => 21, + 2480 => 26, + 2481 => 30, + 2482 => 38, + 2483 => 46, + 2484 => 63, + 2485 => 7, + 2486 => 14, + 2487 => 16, + 2488 => 14, + 2489 => 39, + 2490 => 112, + 2491 => 79, + 2492 => 137, + 2493 => 55, + 2494 => 18, + 2495 => 41, + 2496 => 41, + 2497 => 22, + 2498 => 26, + 2499 => 46, + 2500 => 62, + 2501 => 90, + 2502 => 47, + 2503 => 32, + 2504 => 33, + 2505 => 47, + 2506 => 56, + 2507 => 26, + 2508 => 48, + 2509 => 47, + 2510 => 16, + 2511 => 6, + 2512 => 4, + 2513 => 41, + 2514 => 138, + 2515 => 48, + 2516 => 65, + 2517 => 35, + 2518 => 40, + 2519 => 31, + 2520 => 27, + 2521 => 25, + 2522 => 34, + 2523 => 27, + 2524 => 47, + 2525 => 22, + 2526 => 125, + 2527 => 126, + 2528 => 93, + 2529 => 23, + 2530 => 28, + 2531 => 24, + 2532 => 46, + 2533 => 22, + 2534 => 24, + 2535 => 27, + 2536 => 44, + 2537 => 54, + 2538 => 17, + 2539 => 13, + 2540 => 29, + 2541 => 15, + 2542 => 19, + 2543 => 14, + 2544 => 34, + 2545 => 30, + 2546 => 38, + 2547 => 32, + 2548 => 22, + 2549 => 37, + 2550 => 9, + 2551 => 41, + 2552 => 17, + 2553 => 36, + 2554 => 10, + 2555 => 20, + 2556 => 13, + 2557 => 19, + 2558 => 13, + 2559 => 23, + 2560 => 13, + 2561 => 62, + 2562 => 29, + 2563 => 31, + 2564 => 34, + 2565 => 57, + 2566 => 16, + 2567 => 22, + 2568 => 40, + 2569 => 5, + 2570 => 38, + 2571 => 17, + 2572 => 9, + 2573 => 12, + 2574 => 10, + 2575 => 16, + 2576 => 2, + 2577 => 28, + 2578 => 37, + 2579 => 79, + 2580 => 17, + 2581 => 734, + 2582 => 11, + 2583 => 23, + 2584 => 16, + 2585 => 4, + 2586 => 33, + 2587 => 3, + 2588 => 22, + 2589 => 19, + 2590 => 14, + 2591 => 9, + 2592 => 19, + 2593 => 11, + 2594 => 14, + 2595 => 13, + 2596 => 21, + 2597 => 10, + 2598 => 16, + 2599 => 26, + 2600 => 1, + 2601 => 19, + 2602 => 23, + 2603 => 12, + 2604 => 31, + 2605 => 30, + 2606 => 5, + 2607 => 32, + 2608 => 14, + 2609 => 27, + 2610 => 15, + 2611 => 20, + 2612 => 22, + 2613 => 20, + 2614 => 97, + 2615 => 29, + 2616 => 6, + 2617 => 34, + 2618 => 16, + 2619 => 25, + 2620 => 12, + 2621 => 29, + 2622 => 18, + 2623 => 12, + 2624 => 58, + 2625 => 54, + 2626 => 15, + 2627 => 1, + 2629 => 17, + 2630 => 22, + 2636 => 13, + 2637 => 13, + 2638 => 25, + 2639 => 57, + 2640 => 15, + 2641 => 11, + 2642 => 14, + 2643 => 38, + 2644 => 19, + 2645 => 6, + 2646 => 1, + 2647 => 16, + 2648 => 25, + 2649 => 38, + 2650 => 12, + 2651 => 78, + 2652 => 12, + 2654 => 16, + 2655 => 13, + 2656 => 15, + 2657 => 44, + 2658 => 53, + 16869 => 73, + 21068 => 44, + 21069 => 57, + 21070 => 20, + }; +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm index 6bec115dd..461018639 100644 --- a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm +++ b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm @@ -38,7 +38,7 @@ sub area_types { } sub admin_base_url { - return 'http://www.fiksgatami.no/admin/'; + return 'http://www.fiksgatami.no/admin'; } # If lat/lon are present in the URL, OpenLayers will use that to centre the map. @@ -100,33 +100,26 @@ sub guess_road_operator { return ''; } -sub remove_redundant_councils { +sub remove_redundant_areas { my $self = shift; - my $all_councils = shift; + my $all_areas = shift; # Oslo is both a kommune and a fylke, we only want to show it once - delete $all_councils->{301} # - if $all_councils->{3}; -} - -sub filter_all_council_ids_list { - my $self = shift; - my @all_councils_ids = @_; - - # as above we only want to show Oslo once - return grep { $_ != 301 } @all_councils_ids; + delete $all_areas->{301} + if $all_areas->{3}; } sub short_name { my $self = shift; my ($area, $info) = @_; - if ($area->{name} =~ /^(Os|Nes|V\xe5ler|Sande|B\xf8|Her\xf8y)$/) { + my $name = $area->{name} || $area->name; + + if ($name =~ /^(Os|Nes|V\xe5ler|Sande|B\xf8|Her\xf8y)$/) { my $parent = $info->{$area->{parent_area}}->{name}; - return URI::Escape::uri_escape_utf8("$area->{name}, $parent"); + return URI::Escape::uri_escape_utf8("$name, $parent"); } - my $name = $area->{name}; $name =~ s/ & / and /; $name = URI::Escape::uri_escape_utf8($name); $name =~ s/%20/+/g; diff --git a/perllib/FixMyStreet/Cobrand/FixMindelo.pm b/perllib/FixMyStreet/Cobrand/FixMindelo.pm new file mode 100644 index 000000000..fd3a55c6c --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/FixMindelo.pm @@ -0,0 +1,27 @@ +package FixMyStreet::Cobrand::FixMindelo; +use base 'FixMyStreet::Cobrand::Default'; + +use strict; +use warnings; + +sub site_title { return 'FixMindelo'; } + +sub country { + return 'CV'; +} + +sub languages { [ 'pt-cv,Portuguese,pt_CV', 'en-gb,English,en_GB' ] } +sub language_override { 'pt-cv' } + +sub disambiguate_location { + return { + country => 'cv', + bing_country => 'Cape Verde', + }; +} + +# let staff hide reports +sub users_can_hide { 1 } + +1; + diff --git a/perllib/FixMyStreet/Cobrand/FixMyBarangay.pm b/perllib/FixMyStreet/Cobrand/FixMyBarangay.pm index c53b8e971..c7535f856 100644 --- a/perllib/FixMyStreet/Cobrand/FixMyBarangay.pm +++ b/perllib/FixMyStreet/Cobrand/FixMyBarangay.pm @@ -33,6 +33,9 @@ sub only_authed_can_create { return 1; } +# effectively allows barangay staff to hide reports +sub council_id { return '1,2' ; } + sub areas_on_around { return [ 1, 2 ]; } @@ -41,7 +44,20 @@ sub can_support_problems { return 1; } -sub reports_by_body { 1 } +sub default_show_name { + my $self = shift; + + return 0 if $self->{c}->user->from_council; + return 1; +} + +# makes no sense to send questionnaires since FMB's reporters are mostly staff +sub send_questionnaires { + return 0; +} + +# let staff hide reports in their own barangay +sub users_can_hide { 1 } 1; diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm index 04c137674..ce62e206e 100644 --- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm +++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm @@ -1,13 +1,42 @@ package FixMyStreet::Cobrand::FixMyStreet; use base 'FixMyStreet::Cobrand::UK'; +use constant COUNCIL_ID_BROMLEY => 2482; + # FixMyStreet should return all cobrands sub restriction { return {}; } sub admin_base_url { - return 'https://secure.mysociety.org/admin/bci/'; + return 'https://secure.mysociety.org/admin/bci'; +} + +sub title_list { + my $self = shift; + my $areas = shift; + my $first_area = ( values %$areas )[0]; + + return ["MR", "MISS", "MRS", "MS", "DR"] if $first_area->{id} eq COUNCIL_ID_BROMLEY; + return undef; +} + +sub extra_contact_validation { + my $self = shift; + my $c = shift; + + my %errors; + + $c->stash->{dest} = $c->req->param('dest'); + + $errors{dest} = "Please enter who your message is for" + unless $c->req->param('dest'); + + if ( $c->req->param('dest') eq 'council' || $c->req->param('dest') eq 'update' ) { + $errors{not_for_us} = 1; + } + + return %errors; } 1; diff --git a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm new file mode 100644 index 000000000..f17f7716f --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm @@ -0,0 +1,272 @@ +package FixMyStreet::Cobrand::FixaMinGata; +use base 'FixMyStreet::Cobrand::Default'; + +use strict; +use warnings; + +use Carp; +use mySociety::MaPit; +use FixMyStreet::Geocode::FixaMinGata; +use DateTime; + + +DateTime->DefaultLocale('sv_SE'); + +sub site_title { + my ($self) = @_; + return 'Fixa Min Gata'; +} + +sub path_to_web_templates { + my $self = shift; + return [ + FixMyStreet->path_to( 'templates/web', $self->moniker )->stringify, + FixMyStreet->path_to( 'templates/web/fixmystreet' )->stringify + ]; +} + +sub country { + return 'SE'; +} + +sub languages { [ 'en-gb,English,en_GB', 'sv,Swedish,sv_SE' ] } +sub language_override { 'sv' } + +sub enter_postcode_text { + my ( $self ) = @_; + return _('Enter a nearby postcode, or street name and area'); +} + +# Is also adding language parameter +sub disambiguate_location { + return { + lang => 'sv', + country => 'se', # Is this the right format? /Rikard + }; +} + +sub area_types { + [ 'KOM' ]; +} + +sub admin_base_url { + return 'http://www.fixamingata.se/admin/'; +} + +# If lat/lon are present in the URL, OpenLayers will use that to centre the map. +# Need to specify a zoom to stop it defaulting to null/0. +sub uri { + my ( $self, $uri ) = @_; + + $uri->query_param( zoom => 3 ) + if $uri->query_param('lat') && !$uri->query_param('zoom'); + + return $uri; +} + +sub geocode_postcode { + my ( $self, $s ) = @_; + # Most people write Swedish postcodes like this: + #+ XXX XX, so let's remove the space + # Is this the right place to do this? //Rikard + # This is the right place! // Jonas + $s =~ s/\ //g; # Rikard, remove space in postcode + if ($s =~ /^\d{5}$/) { + my $location = mySociety::MaPit::call('postcode', $s); + if ($location->{error}) { + return { + error => $location->{code} =~ /^4/ + ? _('That postcode was not recognised, sorry.') + : $location->{error} + }; + } + return { + latitude => $location->{wgs84_lat}, + longitude => $location->{wgs84_lon}, + }; + } + return {}; +} + +# Vad gör den här funktionen? Är "Sverige" rätt här? +sub geocoded_string_check { + my ( $self, $s ) = @_; + return 1 if $s =~ /, Sverige/; + return 0; +} + +sub find_closest { + my ( $self, $latitude, $longitude ) = @_; + return FixMyStreet::Geocode::OSM::closest_road_text( $self, $latitude, $longitude ); +} + +# Used by send-reports, calling find_closest, calling OSM geocoding +sub guess_road_operator { + my ( $self, $inforef ) = @_; + + my $highway = $inforef->{highway} || "unknown"; + my $refs = $inforef->{ref} || "unknown"; + return "Trafikverket" + if $highway eq "trunk" || $highway eq "primary"; + + for my $ref (split(/;/, $refs)) { + return "Trafikverket" + if $ref =~ m/E ?\d+/ || $ref =~ m/Fv\d+/i; + } + return ''; +} + +sub remove_redundant_councils { + my $self = shift; + my $all_councils = shift; + + # Oslo is both a kommune and a fylke, we only want to show it once + # Jag tror inte detta är applicerbart på Sverige ;-) //Rikard + #delete $all_councils->{301} # + # if $all_councils->{3}; +} + +sub filter_all_council_ids_list { + my $self = shift; + my @all_councils_ids = @_; + + # as above we only want to show Oslo once + # Rikard kommenterar ut detta. + # return grep { $_ != 301 } @all_councils_ids; + # Rikard: + return @all_councils_ids; # Är detta rätt? //Rikard +} + +sub short_name { + my $self = shift; + my ($area, $info) = @_; + + # Rikard kommenterar ut följande tills vidare... + #if ($area->{name} =~ /^(Os|Nes|V\xe5ler|Sande|B\xf8|Her\xf8y)$/) { + # my $parent = $info->{$area->{parent_area}}->{name}; + # return URI::Escape::uri_escape_utf8("$area->{name}, $parent"); + #} + + my $name = $area->{name}; + $name =~ s/ & / and /; + $name = URI::Escape::uri_escape_utf8($name); + $name =~ s/%20/+/g; + return $name; +} + +# Vad ska vi göra för svenska förhållanden här??? //Rikard +sub council_rss_alert_options { + my $self = shift; + my $all_councils = shift; + my $c = shift; + + my ( @options, @reported_to_options, $fylke, $kommune ); + + foreach ( values %$all_councils ) { + if ( $_->{type} eq 'NKO' ) { + $kommune = $_; + } + else { + $fylke = $_; + } + } + + if ( $fylke->{id} == 3 ) { # Oslo + my $short_name = $self->short_name($fylke, $all_councils); + ( my $id_name = $short_name ) =~ tr/+/_/; + + push @options, + { + type => 'council', + id => sprintf( 'council:%s:%s', $fylke->{id}, $id_name ), + rss_text => + sprintf( _('RSS feed of problems within %s'), $fylke->{name} ), + text => sprintf( _('Problems within %s'), $fylke->{name} ), + uri => $c->uri_for( '/rss/reports', $short_name ), + }; + } + else { + my $short_kommune_name = $self->short_name($kommune, $all_councils); + ( my $id_kommune_name = $short_kommune_name ) =~ tr/+/_/; + + my $short_fylke_name = $self->short_name($fylke, $all_councils); + ( my $id_fylke_name = $short_fylke_name ) =~ tr/+/_/; + + push @options, + { + type => 'area', + id => sprintf( 'area:%s:%s', $kommune->{id}, $id_kommune_name ), + rss_text => + sprintf( _('RSS feed of %s'), $kommune->{name} ), + text => $kommune->{name}, + uri => $c->uri_for( '/rss/area', $short_kommune_name ), + }, + { + type => 'area', + id => sprintf( 'area:%s:%s', $fylke->{id}, $id_fylke_name ), + rss_text => + sprintf( _('RSS feed of %s'), $fylke->{name} ), + text => $fylke->{name}, + uri => $c->uri_for( '/rss/area', $short_fylke_name ), + }; + + push @reported_to_options, + { + type => 'council', + id => sprintf( 'council:%s:%s', $kommune->{id}, $id_kommune_name ), + rss_text => + sprintf( _('RSS feed of %s'), $kommune->{name} ), + text => $kommune->{name}, + uri => $c->uri_for( '/rss/reports', $short_kommune_name ), + }, + { + type => 'council', + id => sprintf( 'council:%s:%s', $fylke->{id}, $id_fylke_name ), + rss_text => + sprintf( _('RSS feed of %s'), $fylke->{name} ), + text => $fylke->{name}, + uri => $c->uri_for( '/rss/reports/', $short_fylke_name ), + }; + } + + return ( + \@options, @reported_to_options + ? \@reported_to_options + : undef + ); + +} + +# Vad ska vi göra för svenska förhållanden här??? //Rikard +sub reports_council_check { + my ( $self, $c, $council ) = @_; + + if ($council eq 'Oslo') { + + # There are two Oslos (kommune and fylke), we only want one of them. + $c->stash->{council} = mySociety::MaPit::call('area', 3); + return 1; + + } elsif ($council =~ /,/) { + + # Some kommunes have the same name, use the fylke name to work out which. + my ($kommune, $fylke) = split /\s*,\s*/, $council; + my $area_types = $c->cobrand->area_types; + my $areas_k = mySociety::MaPit::call('areas', $kommune, type => $area_types); + my $areas_f = mySociety::MaPit::call('areas', $fylke, type => $area_types); + if (keys %$areas_f == 1) { + ($fylke) = values %$areas_f; + foreach (values %$areas_k) { + if ($_->{name} eq $kommune && $_->{parent_area} == $fylke->{id}) { + $c->stash->{council} = $_; + return 1; + } + } + } + # If we're here, we've been given a bad name. + $c->detach( 'redirect_index' ); + + } +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/Hart.pm b/perllib/FixMyStreet/Cobrand/Hart.pm new file mode 100644 index 000000000..cab834b69 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Hart.pm @@ -0,0 +1,72 @@ +package FixMyStreet::Cobrand::Hart; +use parent 'FixMyStreet::Cobrand::UKCouncils'; + +use strict; +use warnings; + +sub council_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'; } +sub is_two_tier { return 1; } + +# Different to councils parent due to this being a two-tier council. If we get +# more, this can be genericised in the parent. +sub problems_clause { + return { bodies_str => { like => '%2333%' } }; +} + +sub path_to_web_templates { + my $self = shift; + return [ + FixMyStreet->path_to( 'templates/web', $self->moniker )->stringify, + FixMyStreet->path_to( 'templates/web/fixmystreet' )->stringify + ]; +} + +sub disambiguate_location { + my $self = shift; + my $string = shift; + + my $town = 'Hart, Hampshire'; + + return { + %{ $self->SUPER::disambiguate_location() }, + town => $town, + # these are taken from mapit http://mapit.mysociety.org/area/2333/geometry -- should be automated? + centre => '51.284839,-0.8974600', + span => '0.180311,0.239375', + bounds => [ 51.186005, -1.002295, 51.366316, -0.762920 ], + }; +} + +sub example_places { + return ( 'GU51 4JX', 'Primrose Drive' ); +} + +sub hidden_categories { + return ( + 'Graffiti on bridges/subways', + ); +} + +sub send_questionnaires { + return 0; +} + +sub ask_ever_reported { + return 0; +} + +sub contact_email { + my $self = shift; + return join( '@', 'info', 'hart.gov.uk' ); +} +sub contact_name { 'Hart District Council (do not reply)'; } + +sub default_map_zoom { 3 } + +sub reports_per_page { return 20; } + +1; + diff --git a/perllib/FixMyStreet/Cobrand/LichfieldDC.pm b/perllib/FixMyStreet/Cobrand/LichfieldDC.pm index b3dbad089..6a534fc18 100644 --- a/perllib/FixMyStreet/Cobrand/LichfieldDC.pm +++ b/perllib/FixMyStreet/Cobrand/LichfieldDC.pm @@ -8,11 +8,12 @@ sub council_id { return 2434; } sub council_area { return 'Lichfield district'; } sub council_name { return 'Lichfield District Council'; } sub council_url { return 'lichfielddc'; } +sub is_two_tier { return 1; } # Different to councils parent due to this being a two-tier council. If we get # more, this can be genericised in the parent. sub problems_clause { - return { council => { like => '%2434%' } }; + return { bodies_str => { like => '%2434%' } }; } # FIXME - need to double check this is all correct @@ -26,17 +27,6 @@ sub disambiguate_location { }; } -# If we ever link to a county problem report, needs to be to main FixMyStreet -sub base_url_for_report { - my ( $self, $report ) = @_; - my %councils = map { $_ => 1 } @{$report->councils}; - if ( $councils{2434} ) { - return $self->base_url; - } else { - return FixMyStreet->config('BASE_URL'); - } -} - sub map_type { return 'OSM'; } diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm new file mode 100644 index 000000000..33611b219 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm @@ -0,0 +1,65 @@ +package FixMyStreet::Cobrand::Oxfordshire; +use base 'FixMyStreet::Cobrand::UKCouncils'; + +use strict; +use warnings; + +sub council_id { return 2237; } +sub council_area { return 'Oxfordshire'; } +sub council_name { return 'Oxfordshire County Council'; } +sub council_url { return 'oxfordshire'; } +sub is_two_tier { return 1; } + +sub base_url { + return FixMyStreet->config('BASE_URL') if FixMyStreet->config('STAGING_SITE'); + return 'http://fixmystreet.oxfordshire.gov.uk'; +} + +# Different to councils parent due to this being a two-tier council. If we get +# more, this can be genericised in the parent. +sub problems_clause { + return { bodies_str => { like => '%2237%' } }; +} + +sub path_to_web_templates { + my $self = shift; + return [ + FixMyStreet->path_to( 'templates/web', $self->moniker )->stringify, + FixMyStreet->path_to( 'templates/web/fixmystreet' )->stringify + ]; +} + +sub enter_postcode_text { + my ($self) = @_; + return 'Enter an Oxfordshire postcode, or street name and area'; +} + +sub disambiguate_location { + my $self = shift; + my $string = shift; + return { + %{ $self->SUPER::disambiguate_location() }, + town => 'Oxfordshire', + centre => '51.765765,-1.322324', + span => '0.709058,0.849434', + bounds => [ 51.459413, -1.719500, 52.168471, -0.870066 ], + }; +} + +sub example_places { + return ( 'OX20 1SZ', 'Park St, Woodstock' ); +} + +# don't send questionnaires to people who used the OCC cobrand to report their problem +sub send_questionnaires { return 0; } + +# increase map zoom level so street names are visible +sub default_map_zoom { return 3; } + +# let staff hide OCC reports +sub users_can_hide { return 1; } + +sub default_show_name { 0 } + +1; + diff --git a/perllib/FixMyStreet/Cobrand/SeeSomething.pm b/perllib/FixMyStreet/Cobrand/SeeSomething.pm index e445ff435..88d5f6b78 100644 --- a/perllib/FixMyStreet/Cobrand/SeeSomething.pm +++ b/perllib/FixMyStreet/Cobrand/SeeSomething.pm @@ -14,12 +14,12 @@ sub site_title { return 'See Something, Say Something'; } sub site_restriction { my $self = shift; - return { council => { IN => $self->council_id } }; + return { bodies_str => { IN => $self->council_id } }; } sub problems_clause { my $self = shift; - return { council => { IN => $self->council_id } }; + return { bodies_str => { IN => $self->council_id } }; } sub path_to_web_templates { @@ -30,10 +30,10 @@ sub path_to_web_templates { ]; } -sub council_check { +sub area_check { my ( $self, $params, $context ) = @_; - my $councils = $params->{all_councils}; + my $councils = $params->{all_areas}; my $council_match = grep { $councils->{$_} } @{ $self->council_id }; if ($council_match) { @@ -78,7 +78,7 @@ sub never_confirm_reports { 1; } sub allow_anonymous_reports { 1; } -sub anonymous_account { return { name => 'anon user', email => 'anon@example.com' }; } +sub anonymous_account { return { name => 'Anonymous Submission', email => FixMyStreet->config('DO_NOT_REPLY_EMAIL') }; } sub admin_pages { my $self = shift; diff --git a/perllib/FixMyStreet/Cobrand/Southampton.pm b/perllib/FixMyStreet/Cobrand/Southampton.pm index b7374149a..4e068a8c3 100644 --- a/perllib/FixMyStreet/Cobrand/Southampton.pm +++ b/perllib/FixMyStreet/Cobrand/Southampton.pm @@ -20,5 +20,7 @@ sub disambiguate_location { }; } +sub send_questionnaires { return 0; } + 1; diff --git a/perllib/FixMyStreet/Cobrand/Stevenage.pm b/perllib/FixMyStreet/Cobrand/Stevenage.pm index f7a9ccd84..560baba37 100644 --- a/perllib/FixMyStreet/Cobrand/Stevenage.pm +++ b/perllib/FixMyStreet/Cobrand/Stevenage.pm @@ -8,6 +8,7 @@ sub council_id { return 2347; } sub council_area { return 'Stevenage'; } sub council_name { return 'Stevenage Council'; } sub council_url { return 'stevenage'; } +sub is_two_tier { return 1; } sub path_to_web_templates { my $self = shift; diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm index 4eee1869e..4c94fb34d 100644 --- a/perllib/FixMyStreet/Cobrand/UK.pm +++ b/perllib/FixMyStreet/Cobrand/UK.pm @@ -1,6 +1,7 @@ package FixMyStreet::Cobrand::UK; use base 'FixMyStreet::Cobrand::Default'; +use mySociety::MaPit; use mySociety::VotingArea; sub path_to_web_templates { @@ -11,7 +12,6 @@ sub path_to_web_templates { sub country { return 'GB'; } sub area_types { [ 'DIS', 'LBO', 'MTD', 'UTA', 'CTY', 'COI', 'LGD' ] } sub area_types_children { $mySociety::VotingArea::council_child_types } -sub area_min_generation { 10 } sub enter_postcode_text { my ( $self ) = @_; @@ -31,8 +31,11 @@ sub disambiguate_location { }; } -sub _fallback_council_sender { - my ( $self, $area_id, $area_info, $category ) = @_; +sub _fallback_body_sender { + my ( $self, $body, $category ) = @_; + + my $first_area = $body->body_areas->first->area_id; + my $area_info = mySociety::MaPit::call('area', $first_area); return { method => 'London' } if $area_info->{type} eq 'LBO'; return { method => 'NI' } if $area_info->{type} eq 'LGD'; return { method => 'Email' }; @@ -41,11 +44,12 @@ sub _fallback_council_sender { sub process_extras { my $self = shift; my $ctx = shift; - my $area_id = shift; + my $body_id = shift; my $extra = shift; my $fields = shift || []; - if ( $area_id eq '2482' ) { + # XXX Hardcoded body ID matching mapit area ID + if ( $body_id eq '2482' ) { my @fields = ( 'fms_extra_title', @$fields ); for my $field ( @fields ) { my $value = $ctx->request->param( $field ); @@ -97,46 +101,32 @@ sub geocode_postcode { return {}; } -sub remove_redundant_councils { +sub remove_redundant_areas { my $self = shift; - my $all_councils = shift; - - # Ipswich & St Edmundsbury are responsible for everything in their - # areas, not Suffolk - delete $all_councils->{2241} - if $all_councils->{2446} # - || $all_councils->{2443}; + my $all_areas = shift; # Norwich is responsible for everything in its areas, not Norfolk - delete $all_councils->{2233} # - if $all_councils->{2391}; + delete $all_areas->{2233} # + if $all_areas->{2391}; } -sub filter_all_council_ids_list { +sub short_name { my $self = shift; - my @all_councils_ids = @_; + my ($area) = @_; - # Ignore the four council areas introduced because of generation 15 - # (where we put the new boundaries under the old IDs) - return grep { $_ < 141648 || $_ > 141651 } @all_councils_ids; -} + my $name = $area->{name} || $area->name; -sub short_name { - my $self = shift; - my ($area, $info) = @_; - # Special case Durham as it's the only place with two councils of the same name - return 'Durham+County' if $area->{name} eq 'Durham County Council'; - return 'Durham+City' if $area->{name} eq 'Durham City Council'; - - my $name = $area->{name}; - $name =~ s/ (Borough|City|District|County) Council$//; - $name =~ s/ Council$//; - $name =~ s/ & / and /; - $name =~ s{/}{_}g; - $name = URI::Escape::uri_escape_utf8($name); - $name =~ s/%20/+/g; - return $name; + # Special case Durham as it's the only place with two councils of the same name + return 'Durham+County' if $name eq 'Durham County Council'; + return 'Durham+City' if $name eq 'Durham City Council'; + $name =~ s/ (Borough|City|District|County) Council$//; + $name =~ s/ Council$//; + $name =~ s/ & / and /; + $name =~ s{/}{_}g; + $name = URI::Escape::uri_escape_utf8($name); + $name =~ s/%20/+/g; + return $name; } sub find_closest { @@ -174,11 +164,11 @@ sub reports_body_check { if (length($code) == 6) { my $council = mySociety::MaPit::call( 'area', $area->{parent_area} ); $c->stash->{ward} = $area; - $c->stash->{council} = $council; + $c->stash->{body} = $council; } else { - $c->stash->{council} = $area; + $c->stash->{body} = $area; } - $c->detach( 'redirect_area' ); + $c->detach( 'redirect_body' ); } # New ONS codes @@ -188,29 +178,30 @@ sub reports_body_check { if ($code =~ /^(E05|W05|S13)/) { my $council = mySociety::MaPit::call( 'area', $area->{parent_area} ); $c->stash->{ward} = $area; - $c->stash->{council} = $council; - $c->detach( 'redirect_area' ); + $c->stash->{body} = $council; + $c->detach( 'redirect_body' ); } elsif ($code =~ /^(W06|S12|E0[6-9]|E10)/) { - $c->stash->{council} = $area; - $c->detach( 'redirect_area' ); + $c->stash->{body} = $area; + $c->detach( 'redirect_body' ); } } + return; } sub council_rss_alert_options { my $self = shift; - my $all_councils = shift; - my $c = shift; + my $all_areas = shift; + my $c = shift; my %councils = map { $_ => 1 } @{$self->area_types}; - my $num_councils = scalar keys %$all_councils; + my $num_councils = scalar keys %$all_areas; my ( @options, @reported_to_options ); if ( $num_councils == 1 or $num_councils == 2 ) { my ($council, $ward); - foreach (values %$all_councils) { + foreach (values %$all_areas) { if ($councils{$_->{type}}) { $council = $_; $council->{short_name} = $self->short_name( $council ); @@ -249,7 +240,7 @@ sub council_rss_alert_options { } elsif ( $num_councils == 4 ) { # Two-tier council my ($county, $district, $c_ward, $d_ward); - foreach (values %$all_councils) { + foreach (values %$all_areas) { $_->{short_name} = $self->short_name( $_ ); ( $_->{id_name} = $_->{short_name} ) =~ tr/+/_/; if ($_->{type} eq 'CTY') { @@ -320,7 +311,7 @@ sub council_rss_alert_options { }; } else { - throw Error::Simple('An area with three tiers of council? Impossible! '. join('|',keys %$all_councils)); + throw Error::Simple('An area with three tiers of council? Impossible! '. join('|',keys %$all_areas)); } return ( \@options, @reported_to_options ? \@reported_to_options : undef ); diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm index a9ebb1b3f..ec3423f35 100644 --- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm +++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm @@ -13,7 +13,7 @@ sub is_council { sub site_restriction { my $self = shift; - return { council => sprintf('%d', $self->council_id) }; + return { bodies_str => sprintf('%d', $self->council_id) }; } sub site_key { my $self = shift; @@ -27,7 +27,7 @@ sub restriction { # Different function to site_restriction due to two-tier use sub problems_clause { my $self = shift; - return { council => sprintf('%d', $self->council_id) }; + return { bodies_str => sprintf('%d', $self->council_id) }; } sub problems { @@ -51,10 +51,10 @@ sub enter_postcode_text { return 'Enter a ' . $self->council_area . ' postcode, or street name and area'; } -sub council_check { +sub area_check { my ( $self, $params, $context ) = @_; - my $councils = $params->{all_councils}; + my $councils = $params->{all_areas}; my $council_match = defined $councils->{$self->council_id}; if ($council_match) { return 1; @@ -76,8 +76,21 @@ Please visit <a href=\"$url\">the main FixMyStreet site</a>."; } # All reports page only has the one council. -sub all_councils_report { - return 0; +sub all_reports_single_body { + my $self = shift; + return { name => $self->council_name }; +} + +sub reports_body_check { + my ( $self, $c, $code ) = @_; + + # We want to make sure we're only on our page. + unless ( $self->council_name =~ /^\Q$code\E/ ) { + $c->res->redirect( 'http://www.fixmystreet.com' . $c->req->uri->path_query, 301 ); + $c->detach(); + } + + return; } sub recent_photos { @@ -86,4 +99,26 @@ sub recent_photos { return $self->problems->recent_photos( $num, $lat, $lon, $dist ); } +# Returns true if the cobrand owns the problem. +sub owns_problem { + my ($self, $report) = @_; + my $bodies = $report->bodies; + my %areas = map { %{$_->areas} } values %$bodies; + return $areas{$self->council_id} ? 1 : undef; +} + +# If we ever link to a county problem report, needs to be to main FixMyStreet +sub base_url_for_report { + my ( $self, $report ) = @_; + if ( $self->is_two_tier ) { + if ( $self->owns_problem( $report ) ) { + return $self->base_url; + } else { + return FixMyStreet->config('BASE_URL'); + } + } else { + return $self->base_url; + } +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/ZeroTB.pm b/perllib/FixMyStreet/Cobrand/ZeroTB.pm new file mode 100644 index 000000000..087bf0912 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/ZeroTB.pm @@ -0,0 +1,61 @@ +package FixMyStreet::Cobrand::ZeroTB; +use base 'FixMyStreet::Cobrand::Default'; + +use strict; +use warnings; + +sub site_title { return 'ZeroTB'; } + +sub enter_postcode_text { return _ ('Enter a nearby street name and area, postal code or district in Delhi'); } + +sub country { + return 'IN'; +} + +sub languages { [ 'en-gb,English,en_GB' ] } +sub language_override { 'en-gb' } + +sub disambiguate_location { + return { + country => 'in', + town => 'Delhi', + bounds => [ 28.404625000000024, 76.838845800000072, 28.884380600000028, 77.347877500000067 ], + }; +} + +sub only_authed_can_create { return 1; } +sub allow_photo_display { return 0; } +sub allow_photo_upload{ return 0; } +sub send_questionnaires { return 0; } +sub on_map_default_max_pin_age { return 0; } +sub never_confirm_updates { 1; } +sub include_time_in_update_alerts { 1; } + +sub pin_colour { + return 'clinic'; +} + +sub path_to_pin_icons { + return '/cobrands/zerotb/images/'; +} + +sub get_clinic_list { + my $self = shift; + + return $self->problems->search({ state => 'confirmed' }, { order_by => 'title' }); +} + +sub prettify_dt { + my ( $self, $dt, $type ) = @_; + $type ||= ''; + + if ( $type eq 'alert' ) { + return $dt->strftime('%H:%M %Y-%m-%d'); + } else { + return Utils::prettify_dt( $dt, $type ); + } + +} + +1; + diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm new file mode 100644 index 000000000..087d9046b --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Zurich.pm @@ -0,0 +1,819 @@ +package FixMyStreet::Cobrand::Zurich; +use base 'FixMyStreet::Cobrand::Default'; + +use DateTime; +use POSIX qw(strcoll); +use RABX; + +use strict; +use warnings; + +=head1 NAME + +Zurich FixMyStreet cobrand + +=head1 DESCRIPTION + +This module provides the specific functionality for the Zurich FMS cobrand. + +=head1 DEVELOPMENT NOTES + +The admin for Zurich is different to the other cobrands. To access it you need +to be logged in as a user associated with an appropriate body. + +You can create the bodies needed to develop by running the 't/cobrand/zurich.t' +test script with the three C<$mech->delete...> lines at the end commented out. +This should leave you with the bodies and users correctly set up. + +The entries will be something like this (but with different ids). + + Bodies: + id | name | parent | endpoint + ----+---------------+--------+--------------------------- + 1 | Zurich | | + 2 | Division 1 | 1 | division@example.org + 3 | Subdivision A | 2 | subdivision@example.org + 4 | External Body | | external_body@example.org + + Users: + id | email | from_body + ----+------------------+----------- + 1 | super@example.org| 1 + 2 | dm1@example.org | 2 + 3 | sdm1@example.org | 3 + +The passwords for the users is 'secret'. + +Note: the password hashes are salted with the user's id so cannot be easily +changed. High ids have been used so that it should not conflict with anything +you already have, and the countres set so that they shouldn't in future. + +=cut + +sub shorten_recency_if_new_greater_than_fixed { + return 0; +} + +sub pin_colour { + my ( $self, $p, $context ) = @_; + return 'green' if $p->is_fixed || $p->is_closed; + return 'red' if $p->state eq 'unconfirmed' || $p->state eq 'confirmed'; + return 'yellow'; +} + +# This isn't used +sub find_closest { + my ( $self, $latitude, $longitude, $problem ) = @_; + return ''; +} + +sub enter_postcode_text { + my ( $self ) = @_; + return _('Enter a Zürich street name'); +} + +sub example_places { + return [ 'Langstrasse', 'Basteiplatz' ]; +} + +sub languages { [ 'de-ch,Deutsch,de_CH', 'en-gb,English,en_GB' ] } +sub language_override { 'de-ch' } + +# If lat/lon are in the URI, we must have zoom as well, otherwise OpenLayers defaults to 0. +sub uri { + my ( $self, $uri ) = @_; + + $uri->query_param( zoom => 6 ) + if $uri->query_param('lat') && !$uri->query_param('zoom'); + return $uri; +} + +sub prettify_dt { + my $self = shift; + my $dt = shift; + + return Utils::prettify_dt( $dt, 'zurich' ); +} + +# problem already has a concept of is_fixed/is_closed, but Zurich has different +# workflow for this here. +# +# TODO: look at more elegant way of doing this, for example having ::DB::Problem +# consider cobrand specific state config? + +sub zurich_closed_states { + my $states = { + 'fixed - council' => 1, + 'closed' => 1, + 'hidden' => 1, + }; + + return wantarray ? keys %{ $states } : $states; +} + +sub problem_is_closed { + my ($self, $problem) = @_; + return exists $self->zurich_closed_states->{ $problem->state } ? 1 : 0; +} + +sub problem_as_hashref { + my $self = shift; + my $problem = shift; + my $ctx = shift; + + my $hashref = $problem->as_hashref( $ctx ); + + if ( $problem->state eq 'unconfirmed' ) { + for my $var ( qw( photo detail state state_t is_fixed meta ) ) { + delete $hashref->{ $var }; + } + $hashref->{detail} = _('This report is awaiting moderation.'); + $hashref->{title} = _('This report is awaiting moderation.'); + $hashref->{state} = 'submitted'; + $hashref->{state_t} = _('Submitted'); + } else { + if ( $problem->state eq 'confirmed' ) { + $hashref->{state} = 'open'; + $hashref->{state_t} = _('Open'); + } elsif ( $problem->is_fixed ) { + $hashref->{state} = 'closed'; + $hashref->{state_t} = _('Closed'); + } elsif ( $problem->state eq 'in progress' || $problem->state eq 'planned' ) { + $hashref->{state} = 'in progress'; + $hashref->{state_t} = _('In progress'); + } + } + + return $hashref; +} + +sub updates_as_hashref { + my $self = shift; + my $problem = shift; + my $ctx = shift; + + my $hashref = {}; + + if ( $problem->state eq 'fixed - council' || $problem->state eq 'closed' ) { + $hashref->{update_pp} = $self->prettify_dt( $problem->lastupdate ); + + if ( $problem->state eq 'fixed - council' ) { + $hashref->{details} = FixMyStreet::App::View::Web->add_links( $ctx, $problem->extra ? $problem->extra->{public_response} : '' ); + } elsif ( $problem->state eq 'closed' ) { + $hashref->{details} = sprintf( _('Assigned to %s'), $problem->body($ctx)->name ); + } + } + + return $hashref; +} + +sub allow_photo_display { + my ( $self, $r ) = @_; + if (ref($r) ne 'HASH') { + return $r->extra && $r->extra->{publish_photo}; + } + my $extra = $r->{extra}; + utf8::encode($extra) if utf8::is_utf8($extra); + my $h = new IO::String($extra); + $extra = RABX::wire_rd($h); + return $extra->{publish_photo}; +} + +sub show_unconfirmed_reports { + 1; +} + +sub get_body_sender { + my ( $self, $body, $category ) = @_; + return { method => 'Zurich' }; +} + +# Report overdue functions + +my %public_holidays = map { $_ => 1 } ( + '2013-01-01', '2013-01-02', '2013-03-29', '2013-04-01', + '2013-04-15', '2013-05-01', '2013-05-09', '2013-05-20', + '2013-08-01', '2013-09-09', '2013-12-25', '2013-12-26', + '2014-01-01', '2014-01-02', '2014-04-18', '2014-04-21', + '2014-04-28', '2014-05-01', '2014-05-29', '2014-06-09', + '2014-08-01', '2014-09-15', '2014-12-25', '2014-12-26', +); + +sub is_public_holiday { + my $dt = shift; + return $public_holidays{$dt->ymd}; +} + +sub is_weekend { + my $dt = shift; + return $dt->dow > 5; +} + +sub add_days { + my ( $dt, $days ) = @_; + $dt = $dt->clone; + while ( $days > 0 ) { + $dt->add ( days => 1 ); + next if is_public_holiday($dt) or is_weekend($dt); + $days--; + } + return $dt; +} + +sub sub_days { + my ( $dt, $days ) = @_; + $dt = $dt->clone; + while ( $days > 0 ) { + $dt->subtract ( days => 1 ); + next if is_public_holiday($dt) or is_weekend($dt); + $days--; + } + return $dt; +} + +sub overdue { + my ( $self, $problem ) = @_; + + my $w = $problem->created; + return 0 unless $w; + + # call with previous state + if ( $problem->state eq 'unconfirmed' ) { + # One working day + $w = add_days( $w, 1 ); + return $w < DateTime->now() ? 1 : 0; + } elsif ( $problem->state eq 'confirmed' || $problem->state eq 'in progress' || $problem->state eq 'planned' ) { + # States which affect the subdiv_overdue statistic. TODO: this may no longer be required + # Six working days from creation + $w = add_days( $w, 6 ); + return $w < DateTime->now() ? 1 : 0; + + # call with new state + } elsif ( $self->problem_is_closed($problem) ) { + # States which affect the closed_overdue statistic + # Five working days from moderation (so 6 from creation) + + $w = add_days( $w, 6 ); + return $w < DateTime->now() ? 1 : 0; + } else { + return 0; + } +} + +sub get_or_check_overdue { + my ($self, $problem) = @_; + + # use the cached version is it exists (e.g. when called from template) + my $extra = $problem->extra; + if (exists $extra->{closed_overdue} and defined $extra->{closed_overdue}) { + return $extra->{closed_overdue} + } + return $self->overdue($problem); +} + +sub set_problem_state { + my ($self, $c, $problem, $new_state) = @_; + return if $new_state eq $problem->state; + $problem->state( $new_state ); + $c->forward( 'log_edit', [ $problem->id, 'problem', "state change to $new_state" ] ); +} + +# Specific administrative displays + +sub admin_pages { + my $self = shift; + my $c = $self->{c}; + + my $type = $c->stash->{admin_type}; + my $pages = { + 'summary' => [_('Summary'), 0], + 'reports' => [_('Reports'), 2], + 'report_edit' => [undef, undef], + 'update_edit' => [undef, undef], + }; + return $pages if $type eq 'sdm'; + + $pages = { %$pages, + 'bodies' => [_('Bodies'), 1], + 'body' => [undef, undef], + 'body_edit' => [undef, undef], + }; + return $pages if $type eq 'dm'; + + $pages = { %$pages, + 'users' => [_('Users'), 3], + 'stats' => [_('Stats'), 4], + 'user_edit' => [undef, undef], + }; + return $pages if $type eq 'super'; +} + +sub admin_type { + my $self = shift; + my $c = $self->{c}; + my $body = $c->user->from_body; + $c->stash->{body} = $body; + + my $type; + my $parent = $body->parent; + if (!$parent) { + $type = 'super'; + } else { + my $grandparent = $parent->parent; + $type = $grandparent ? 'sdm' : 'dm'; + } + + $c->stash->{admin_type} = $type; + return $type; +} + +sub admin { + my $self = shift; + my $c = $self->{c}; + my $type = $c->stash->{admin_type}; + + if ($type eq 'dm') { + $c->stash->{template} = 'admin/index-dm.html'; + + my $body = $c->stash->{body}; + my @children = map { $_->id } $body->bodies->all; + my @all = (@children, $body->id); + + my $order = $c->req->params->{o} || 'created'; + my $dir = defined $c->req->params->{d} ? $c->req->params->{d} : 1; + $c->stash->{order} = $order; + $c->stash->{dir} = $dir; + $order .= ' desc' if $dir; + + # XXX No multiples or missing bodies + $c->stash->{unconfirmed} = $c->cobrand->problems->search({ + state => [ 'unconfirmed', 'confirmed' ], + bodies_str => $c->stash->{body}->id, + }, { + order_by => $order, + }); + $c->stash->{approval} = $c->cobrand->problems->search({ + state => 'planned', + bodies_str => $c->stash->{body}->id, + }, { + order_by => $order, + }); + + my $page = $c->req->params->{p} || 1; + $c->stash->{other} = $c->cobrand->problems->search({ + state => { -not_in => [ 'unconfirmed', 'confirmed', 'planned' ] }, + bodies_str => \@all, + }, { + order_by => $order, + })->page( $page ); + $c->stash->{pager} = $c->stash->{other}->pager; + + } elsif ($type eq 'sdm') { + $c->stash->{template} = 'admin/index-sdm.html'; + + my $body = $c->stash->{body}; + + my $order = $c->req->params->{o} || 'created'; + my $dir = defined $c->req->params->{d} ? $c->req->params->{d} : 1; + $c->stash->{order} = $order; + $c->stash->{dir} = $dir; + $order .= ' desc' if $dir; + + # XXX No multiples or missing bodies + $c->stash->{reports_new} = $c->cobrand->problems->search( { + state => 'in progress', + bodies_str => $body->id, + }, { + order_by => $order + } ); + $c->stash->{reports_unpublished} = $c->cobrand->problems->search( { + state => 'planned', + bodies_str => $body->parent->id, + }, { + order_by => $order + } ); + + my $page = $c->req->params->{p} || 1; + $c->stash->{reports_published} = $c->cobrand->problems->search( { + state => 'fixed - council', + bodies_str => $body->parent->id, + }, { + order_by => $order + } )->page( $page ); + $c->stash->{pager} = $c->stash->{reports_published}->pager; + } +} + +sub admin_report_edit { + my $self = shift; + my $c = $self->{c}; + my $type = $c->stash->{admin_type}; + + my $problem = $c->stash->{problem}; + my $body = $c->stash->{body}; + + if ($type ne 'super') { + my %allowed_bodies = map { $_->id => 1 } ( $body->bodies->all, $body ); + $c->detach( '/page_error_404_not_found' ) + unless $allowed_bodies{$problem->bodies_str}; + } + + if ($type eq 'super') { + + my @bodies = $c->model('DB::Body')->all(); + @bodies = sort { strcoll($a->name, $b->name) } @bodies; + $c->stash->{bodies} = \@bodies; + + # Can change category to any other + my @categories = $c->model('DB::Contact')->not_deleted->all; + $c->stash->{categories} = [ map { $_->category } @categories ]; + + } elsif ($type eq 'dm') { + + # Can assign to: + my @bodies = $c->model('DB::Body')->search( [ + { 'me.parent' => $body->parent->id }, # Other DMs on the same level + { 'me.parent' => $body->id }, # Their subdivisions + { 'me.parent' => undef, 'bodies.id' => undef }, # External bodies + ], { join => 'bodies', distinct => 1 } ); + @bodies = sort { strcoll($a->name, $b->name) } @bodies; + $c->stash->{bodies} = \@bodies; + + # Can change category to any other + my @categories = $c->model('DB::Contact')->not_deleted->all; + $c->stash->{categories} = [ map { $_->category } @categories ]; + + } + + # If super or sdm check that the token is correct before proceeding + if ( ($type eq 'super' || $type eq 'dm') && $c->req->param('submit') ) { + $c->forward('check_token'); + } + + # All types of users can add internal notes + if ( ($type eq 'super' || $type eq 'dm' || $type eq 'sdm') && $c->req->param('submit') ) { + # If there is a new note add it as a comment to the problem (with is_internal_note set true in extra). + if ( my $new_internal_note = $c->req->params->{new_internal_note} ) { + $problem->add_to_comments( { + text => $new_internal_note, + user => $c->user->obj, + state => 'hidden', # seems best fit, should not be shown publicly + mark_fixed => 0, + anonymous => 1, + extra => { is_internal_note => 1 }, + } ); + } + } + + # Problem updates upon submission + if ( ($type eq 'super' || $type eq 'dm') && $c->req->param('submit') ) { + # Predefine the hash so it's there for lookups + my $extra = $problem->extra || {}; + $extra->{publish_photo} = $c->req->params->{publish_photo} || 0; + $extra->{third_personal} = $c->req->params->{third_personal} || 0; + # Make sure we have a copy of the original detail field + $extra->{original_detail} = $problem->detail if !$extra->{original_detail} && $c->req->params->{detail} && $problem->detail ne $c->req->params->{detail}; + + # Some changes will be accompanied by an internal note, which if needed + # should be stored in this variable. + my $internal_note_text = ""; + + # Workflow things + my $redirect = 0; + my $new_cat = $c->req->params->{category}; + if ( $new_cat && $new_cat ne $problem->category ) { + my $cat = $c->model('DB::Contact')->search( { category => $c->req->params->{category} } )->first; + my $old_cat = $problem->category; + $problem->category( $new_cat ); + $problem->external_body( undef ); + $problem->bodies_str( $cat->body_id ); + $problem->whensent( undef ); + $extra->{changed_category} = 1; + $internal_note_text = "Weitergeleitet von $old_cat an $new_cat"; + $redirect = 1 if $cat->body_id ne $body->id; + } elsif ( my $subdiv = $c->req->params->{body_subdivision} ) { + $extra->{moderated_overdue} //= $self->overdue( $problem ); + $self->set_problem_state($c, $problem, 'in progress'); + $problem->external_body( undef ); + $problem->bodies_str( $subdiv ); + $problem->whensent( undef ); + $redirect = 1; + } elsif ( my $external = $c->req->params->{body_external} ) { + $extra->{moderated_overdue} //= $self->overdue( $problem ); + $self->set_problem_state($c, $problem, 'closed'); + $extra->{closed_overdue} //= $self->overdue( $problem ); + $problem->external_body( $external ); + $problem->whensent( undef ); + _admin_send_email( $c, 'problem-external.txt', $problem ); + $redirect = 1; + } else { + if (my $state = $c->req->params->{state}) { + + if ($problem->state eq 'unconfirmed' and $state ne 'unconfirmed') { + # only set this for the first state change + $extra->{moderated_overdue} //= $self->overdue( $problem ); + } + + $self->set_problem_state($c, $problem, $state); + + if ($self->problem_is_closed($problem)) { + $extra->{closed_overdue} //= $self->overdue( $problem ); + } + if ( $state eq 'hidden' && $c->req->params->{send_rejected_email} ) { + _admin_send_email( $c, 'problem-rejected.txt', $problem ); + } + } + } + + $problem->extra( $extra ); + $problem->title( $c->req->param('title') ); + $problem->detail( $c->req->param('detail') ); + $problem->latitude( $c->req->param('latitude') ); + $problem->longitude( $c->req->param('longitude') ); + + # Final, public, Update from DM + if (my $update = $c->req->param('status_update')) { + $extra->{public_response} = $update; + $problem->extra( $extra ); + if ($c->req->params->{publish_response}) { + $self->set_problem_state($c, $problem, 'fixed - council'); + $extra->{closed_overdue} = $self->overdue( $problem ); + $problem->extra( { %$extra } ); + _admin_send_email( $c, 'problem-closed.txt', $problem ); + } + } + + $problem->lastupdate( \'ms_current_timestamp()' ); + $problem->update; + + $c->stash->{status_message} = + '<p><em>' . _('Updated!') . '</em></p>'; + + # do this here otherwise lastupdate and confirmed times + # do not display correctly (reloads problem from database, including + # fields modified by the database when saving) + $problem->discard_changes; + + # Create an internal note if required + if ($internal_note_text) { + $problem->add_to_comments( { + text => $internal_note_text, + user => $c->user->obj, + state => 'hidden', # seems best fit, should not be shown publicly + mark_fixed => 0, + anonymous => 1, + extra => { is_internal_note => 1 }, + } ); + } + + if ( $redirect ) { + $c->detach('index'); + } + + $c->stash->{updates} = [ $c->model('DB::Comment') + ->search( { problem_id => $problem->id }, { order_by => 'created' } ) + ->all ]; + + return 1; + } + + if ($type eq 'sdm') { + + # Has cut-down edit template for adding update and sending back up only + $c->stash->{template} = 'admin/report_edit-sdm.html'; + + if ($c->req->param('send_back')) { + $c->forward('check_token'); + + $problem->bodies_str( $body->parent->id ); + $self->set_problem_state($c, $problem, 'confirmed'); + $problem->update; + # log here + $c->res->redirect( '/admin/summary' ); + + } elsif ($c->req->param('submit')) { + $c->forward('check_token'); + + my $db_update = 0; + if ( $c->req->param('latitude') != $problem->latitude || $c->req->param('longitude') != $problem->longitude ) { + $problem->latitude( $c->req->param('latitude') ); + $problem->longitude( $c->req->param('longitude') ); + $db_update = 1; + } + + $problem->update if $db_update; + + # Add new update from status_update + if (my $update = $c->req->param('status_update')) { + FixMyStreet::App->model('DB::Comment')->create( { + text => $update, + user => $c->user->obj, + state => 'unconfirmed', + problem => $problem, + mark_fixed => 0, + problem_state => 'fixed - council', + anonymous => 1, + } ); + } + + $c->stash->{status_message} = '<p><em>' . _('Updated!') . '</em></p>'; + + # If they clicked the no more updates button, we're done. + if ($c->req->param('no_more_updates')) { + my $extra = $problem->extra || {}; + $extra->{subdiv_overdue} = $self->overdue( $problem ); + $problem->extra( $extra ); + $problem->bodies_str( $body->parent->id ); + $problem->whensent( undef ); + $self->set_problem_state($c, $problem, 'planned'); + $problem->update; + $c->res->redirect( '/admin/summary' ); + } + } + + $c->stash->{updates} = [ $c->model('DB::Comment') + ->search( { problem_id => $problem->id }, { order_by => 'created' } ) + ->all ]; + + return 1; + + } + + return 0; + +} + +sub _admin_send_email { + my ( $c, $template, $problem ) = @_; + + return unless $problem->extra && $problem->extra->{email_confirmed}; + + my $to = $problem->name + ? [ $problem->user->email, $problem->name ] + : $problem->user->email; + + # Similar to what SendReport::Zurich does to find address to send to + my $body = ( values %{$problem->bodies} )[0]; + my $sender = $body->endpoint || $c->cobrand->contact_email; + my $sender_name = $c->cobrand->contact_name; # $body->name? + + $c->send_email( $template, { + to => [ $to ], + url => $c->uri_for_email( $problem->url ), + from => [ $sender, $sender_name ], + } ); +} + +sub admin_fetch_all_bodies { + my ( $self, @bodies ) = @_; + + sub tree_sort { + my ( $level, $id, $sorted, $out ) = @_; + + my @sorted; + my $array = $sorted->{$id}; + if ( $level == 0 ) { + @sorted = sort { + # Want Zurich itself at the top. + return -1 if $sorted->{$a->id}; + return 1 if $sorted->{$b->id}; + # Otherwise, by name + strcoll($a->name, $b->name) + } @$array; + } else { + @sorted = sort { strcoll($a->name, $b->name) } @$array; + } + foreach ( @sorted ) { + $_->api_key( $level ); # Misuse + push @$out, $_; + if ($sorted->{$_->id}) { + tree_sort( $level+1, $_->id, $sorted, $out ); + } + } + } + + my %sorted; + foreach (@bodies) { + my $p = $_->parent ? $_->parent->id : 0; + push @{$sorted{$p}}, $_; + } + + my @out; + tree_sort( 0, 0, \%sorted, \@out ); + return @out; +} + +sub admin_stats { + my $self = shift; + my $c = $self->{c}; + + my %date_params; + my $ym = $c->req->params->{ym}; + my ($m, $y) = $ym ? ($ym =~ /^(\d+)\.(\d+)$/) : (); + $c->stash->{ym} = $ym; + if ($y && $m) { + $c->stash->{start_date} = DateTime->new( year => $y, month => $m, day => 1 ); + $c->stash->{end_date} = $c->stash->{start_date} + DateTime::Duration->new( months => 1 ); + $date_params{created} = { '>=', $c->stash->{start_date}, '<', $c->stash->{end_date} }; + } + + my %params = ( + %date_params, + state => [ FixMyStreet::DB::Result::Problem->visible_states() ], + ); + + if ( $c->req->params->{export} ) { + my $problems = $c->model('DB::Problem')->search( + {%date_params}, + { + columns => [ + 'id', 'created', + 'latitude', 'longitude', + 'cobrand', 'category', + 'state', 'user_id', + 'external_body' + ] + } + ); + my $body = "ID,Created,E,N,Category,Status,UserID,External Body\n"; + while ( my $report = $problems->next ) { + my $external_body; + my $body_name = ""; + if ( $external_body = $report->body($c) ) { + $body_name = $external_body->name; + } + $body .= join( ',', + $report->id, $report->created, + $report->local_coords, $report->category, + $report->state, $report->user_id, + "\"$body_name\"" ) + . "\n"; + } + $c->res->content_type('text/csv; charset=utf-8'); + $c->res->body($body); + } + + # Total reports (non-hidden) + my $total = $c->model('DB::Problem')->search( \%params )->count; + # Device for apps (iOS/Android) + my $per_service = $c->model('DB::Problem')->search( \%params, { + select => [ 'service', { count => 'id' } ], + as => [ 'service', 'c' ], + group_by => [ 'service' ], + }); + # Reports solved + my $solved = $c->model('DB::Problem')->search( { state => 'fixed - council', %date_params } )->count; + # Reports marked as spam + my $hidden = $c->model('DB::Problem')->search( { state => 'hidden', %date_params } )->count; + # Reports assigned to third party + my $closed = $c->model('DB::Problem')->search( { state => 'closed', %date_params } )->count; + # Reports moderated within 1 day + my $moderated = $c->model('DB::Problem')->search( { extra => { like => '%moderated_overdue,I1:0%' }, %date_params } )->count; + # Reports solved within 5 days (sent back from subdiv) + my $subdiv_dealtwith = $c->model('DB::Problem')->search( { extra => { like => '%subdiv_overdue,I1:0%' }, %params } )->count; + # Reports solved within 5 days (marked as 'fixed - council', 'closed', or 'hidden' + my $fixed_in_time = $c->model('DB::Problem')->search( { extra => { like => '%closed_overdue,I1:0%' }, %date_params } )->count; + # Reports per category + my $per_category = $c->model('DB::Problem')->search( \%params, { + select => [ 'category', { count => 'id' } ], + as => [ 'category', 'c' ], + group_by => [ 'category' ], + }); + # How many reports have had their category changed by a DM (wrong category chosen by user) + my $changed = $c->model('DB::Problem')->search( { extra => { like => '%changed_category,I1:1%' }, %params } )->count; + # pictures taken + my $pictures_taken = $c->model('DB::Problem')->search( { photo => { '!=', undef }, %params } )->count; + # pictures published + my $pictures_published = $c->model('DB::Problem')->search( { extra => { like => '%publish_photo,I1:1%' }, %params } )->count; + # how many times was a telephone number provided + # XXX => How many users have a telephone number stored + # my $phone = $c->model('DB::User')->search( { phone => { '!=', undef } } )->count; + # how many times was the email address confirmed + my $email_confirmed = $c->model('DB::Problem')->search( { extra => { like => '%email_confirmed%' }, %params } )->count; + # how many times was the name provided + my $name = $c->model('DB::Problem')->search( { name => { '!=', '' }, %params } )->count; + # how many times was the geolocation used vs. addresssearch + # ? + + $c->stash( + per_service => $per_service, + per_category => $per_category, + reports_total => $total, + reports_solved => $solved, + reports_spam => $hidden, + reports_assigned => $closed, + reports_moderated => $moderated, + reports_dealtwith => $fixed_in_time, + reports_category_changed => $changed, + pictures_taken => $pictures_taken, + pictures_published => $pictures_published, + #users_phone => $phone, + email_confirmed => $email_confirmed, + name_provided => $name, + # GEO + ); + + return 1; +} + +1; diff --git a/perllib/FixMyStreet/DB/RABXColumn.pm b/perllib/FixMyStreet/DB/RABXColumn.pm new file mode 100644 index 000000000..5f1583018 --- /dev/null +++ b/perllib/FixMyStreet/DB/RABXColumn.pm @@ -0,0 +1,98 @@ +package FixMyStreet::DB::RABXColumn; + +use strict; +use warnings; + +use IO::String; +use RABX; + +=head1 NAME + +FixMyStreet::DB::RABXColumn + +=head2 DESCRIPTION + +This is a helper component that will setup the RABX serialisation for some +fields. This is useful for when you want to persist some data structure such as +hashrefs etc. + +This code will also change the default FilterColumn behaviour so that whenever +your set a column, or specify a RABX'd column in an ->update the value is saved +to the database. The default behaviour is to check if the value is already set, +and for hashrefs this means that changes to the contents are missed as it is +still the same hashref. + +By putting all this code in one place there is also much less repetition. + +=cut + +# Store which columns are RABX cols. +# $RABX_COLUMNS{$class}{$col} = 1 +my %RABX_COLUMNS = (); + +sub _get_class_identifier { + my $class = ref $_[0] || $_[0]; + $class =~ s/.*?(\w+)$/$1/; + return $class; +} + +=head1 METHODS + +=head2 rabx_column + + # In one of your ::Result:: modules + __PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); + __PACKAGE__->rabx_column('data'); + +This sets up the filtering to and from the database, and also changes the +set_filtered_column behaviour to not trust the cache. + +=cut + +sub rabx_column { + my ($class, $col) = @_; + + # Apply the filtering for this column + $class->filter_column( + $col => { + filter_from_storage => sub { + my $self = shift; + my $ser = shift; + return undef unless defined $ser; + utf8::encode($ser) if utf8::is_utf8($ser); + my $h = new IO::String($ser); + return RABX::wire_rd($h); + }, + filter_to_storage => sub { + my $self = shift; + my $data = shift; + my $ser = ''; + my $h = new IO::String($ser); + RABX::wire_wr( $data, $h ); + return $ser; + }, + } + ); + + # store that this column is a RABX column. + $RABX_COLUMNS{ _get_class_identifier($class) }{$col} = 1; +} + + +sub set_filtered_column { + my ($self, $col, $val) = @_; + + my $class = ref $self; + + # because filtered objects may be expensive to marshall for storage there + # is a cache that attempts to detect if they have changed or not. For us + # this cache breaks things and our marshalling is cheap, so clear it when + # trying set a column. + delete $self->{_filtered_column}{$col} + if $RABX_COLUMNS{ _get_class_identifier($class) }{$col}; + + return $self->next::method($col, $val); +} + + +1; diff --git a/perllib/FixMyStreet/DB/Result/Alert.pm b/perllib/FixMyStreet/DB/Result/Alert.pm index ca9ad45c2..4ce72f873 100644 --- a/perllib/FixMyStreet/DB/Result/Alert.pm +++ b/perllib/FixMyStreet/DB/Result/Alert.pm @@ -48,7 +48,7 @@ __PACKAGE__->belongs_to( "alert_type", "FixMyStreet::DB::Result::AlertType", { ref => "alert_type" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); __PACKAGE__->has_many( "alerts_sent", @@ -60,12 +60,12 @@ __PACKAGE__->belongs_to( "user", "FixMyStreet::DB::Result::User", { id => "user_id" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vump36YxUO4FQi5Do6DwvA +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:d9yIFiTGtbtFaULXZNKstQ # You can replace this text with custom code or comments, and it will be preserved on regeneration @@ -77,22 +77,21 @@ with 'FixMyStreet::Roles::Abuser'; my $tz = DateTime::TimeZone->new( name => "local" ); - -sub whensubscribed_local { - my $self = shift; - - return $self->whensubscribed - ? $self->whensubscribed->set_time_zone($tz) - : $self->whensubscribed; -} - -sub whendisabled_local { - my $self = shift; - - return $self->whendisabled - ? $self->whendisabled->set_time_zone($tz) - : $self->whendisabled; -} +my $tz_f; +$tz_f = DateTime::TimeZone->new( name => FixMyStreet->config('TIME_ZONE') ) + if FixMyStreet->config('TIME_ZONE'); + +my $stz = sub { + my ( $orig, $self ) = ( shift, shift ); + my $s = $self->$orig(@_); + return $s unless $s && UNIVERSAL::isa($s, "DateTime"); + $s->set_time_zone($tz); + $s->set_time_zone($tz_f) if $tz_f; + return $s; +}; + +around whensubscribed => $stz; +around whendisabled => $stz; =head2 confirm diff --git a/perllib/FixMyStreet/DB/Result/AlertSent.pm b/perllib/FixMyStreet/DB/Result/AlertSent.pm index a537c95cd..422e010a9 100644 --- a/perllib/FixMyStreet/DB/Result/AlertSent.pm +++ b/perllib/FixMyStreet/DB/Result/AlertSent.pm @@ -26,12 +26,12 @@ __PACKAGE__->belongs_to( "alert", "FixMyStreet::DB::Result::Alert", { id => "alert_id" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:oN+36hDWJuc0hqkCW9BHOw +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:COwsprqRSNZS1IxJrPYgMQ # You can replace this text with custom code or comments, and it will be preserved on regeneration diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm new file mode 100644 index 000000000..be4adeca9 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/Body.pm @@ -0,0 +1,112 @@ +use utf8; +package FixMyStreet::DB::Result::Body; + +# 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("body"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "body_id_seq", + }, + "name", + { data_type => "text", is_nullable => 0 }, + "parent", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "endpoint", + { data_type => "text", is_nullable => 1 }, + "jurisdiction", + { data_type => "text", is_nullable => 1 }, + "api_key", + { data_type => "text", is_nullable => 1 }, + "send_method", + { data_type => "text", is_nullable => 1 }, + "send_comments", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "comment_user_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "suppress_alerts", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "can_be_devolved", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "send_extended_statuses", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "deleted", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "external_url", + { data_type => "text", is_nullable => 0 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->has_many( + "bodies", + "FixMyStreet::DB::Result::Body", + { "foreign.parent" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); +__PACKAGE__->has_many( + "body_areas", + "FixMyStreet::DB::Result::BodyArea", + { "foreign.body_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); +__PACKAGE__->belongs_to( + "comment_user", + "FixMyStreet::DB::Result::User", + { id => "comment_user_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); +__PACKAGE__->has_many( + "contacts", + "FixMyStreet::DB::Result::Contact", + { "foreign.body_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); +__PACKAGE__->belongs_to( + "parent", + "FixMyStreet::DB::Result::Body", + { id => "parent" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); +__PACKAGE__->has_many( + "users", + "FixMyStreet::DB::Result::User", + { "foreign.from_body" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 18:11:23 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hTOxxiiHmC8nmQK/p8dXhQ + +sub url { + my ( $self, $c ) = @_; + # XXX $areas_info was used here for Norway parent - needs body parents, I guess + return $c->uri_for( '/reports/' . $c->cobrand->short_name( $self ) ); +} + +sub areas { + my $self = shift; + my %ids = map { $_->area_id => 1 } $self->body_areas->all; + return \%ids; +} + +1; diff --git a/perllib/FixMyStreet/DB/Result/BodyArea.pm b/perllib/FixMyStreet/DB/Result/BodyArea.pm new file mode 100644 index 000000000..4447777dc --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/BodyArea.pm @@ -0,0 +1,33 @@ +use utf8; +package FixMyStreet::DB::Result::BodyArea; + +# 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("body_areas"); +__PACKAGE__->add_columns( + "body_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "area_id", + { data_type => "integer", is_nullable => 0 }, +); +__PACKAGE__->add_unique_constraint("body_areas_body_id_area_id_idx", ["body_id", "area_id"]); +__PACKAGE__->belongs_to( + "body", + "FixMyStreet::DB::Result::Body", + { id => "body_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+hzie6kHleUBoEt199c/nQ + + __PACKAGE__->set_primary_key(__PACKAGE__->columns); + +1; diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm index 8c9fea282..e170a5655 100644 --- a/perllib/FixMyStreet/DB/Result/Comment.pm +++ b/perllib/FixMyStreet/DB/Result/Comment.pm @@ -54,6 +54,10 @@ __PACKAGE__->add_columns( { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "problem_state", { data_type => "text", is_nullable => 1 }, + "external_id", + { data_type => "text", is_nullable => 1 }, + "extra", + { data_type => "text", is_nullable => 1 }, "send_fail_count", { data_type => "integer", default_value => 0, is_nullable => 0 }, "send_fail_reason", @@ -62,76 +66,52 @@ __PACKAGE__->add_columns( { data_type => "timestamp", is_nullable => 1 }, "whensent", { data_type => "timestamp", is_nullable => 1 }, - "external_id", - { data_type => "text", is_nullable => 1 }, - "extra", - { data_type => "text", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->belongs_to( "problem", "FixMyStreet::DB::Result::Problem", { id => "problem_id" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); __PACKAGE__->belongs_to( "user", "FixMyStreet::DB::Result::User", { id => "user_id" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-07-11 18:53:26 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tSejJzLxHD/fMWjpa10lfA - -__PACKAGE__->filter_column( - extra => { - filter_from_storage => sub { - my $self = shift; - my $ser = shift; - return undef unless defined $ser; - my $h = new IO::String($ser); - return RABX::wire_rd($h); - }, - filter_to_storage => sub { - my $self = shift; - my $data = shift; - my $ser = ''; - my $h = new IO::String($ser); - RABX::wire_wr( $data, $h ); - return $ser; - }, - } -); +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:D/+UWcF7JO/EkCiJaAHUOw + +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('extra'); use DateTime::TimeZone; use Image::Size; use Moose; use namespace::clean -except => [ 'meta' ]; -use RABX; with 'FixMyStreet::Roles::Abuser'; my $tz = DateTime::TimeZone->new( name => "local" ); -sub created_local { - my $self = shift; +my $tz_f; +$tz_f = DateTime::TimeZone->new( name => FixMyStreet->config('TIME_ZONE') ) + if FixMyStreet->config('TIME_ZONE'); - return $self->created - ? $self->created->set_time_zone($tz) - : $self->created; -} +my $stz = sub { + my ( $orig, $self ) = ( shift, shift ); + my $s = $self->$orig(@_); + return $s unless $s && UNIVERSAL::isa($s, "DateTime"); + $s->set_time_zone($tz); + $s->set_time_zone($tz_f) if $tz_f; + return $s; +}; -sub confirmed_local { - my $self = shift; - - # if confirmed is null then it doesn't get inflated so don't - # try and set the timezone - return $self->confirmed - ? $self->confirmed->set_time_zone($tz) - : $self->confirmed; -} +around created => $stz; +around confirmed => $stz; # You can replace this text with custom code or comments, and it will be preserved on regeneration @@ -146,6 +126,12 @@ sub check_for_errors { $errors{update} = _('Please enter a message') 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 ); + } + return \%errors; } @@ -175,8 +161,8 @@ sub get_photo_params { =head2 meta_problem_state -Returns a string suitable for display in the update meta section. -Mostly removes the '- council/user' bit from fixed states +Returns a string suitable for display lookup in the update meta section. +Removes the '- council/user' bit from fixed states. =cut diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm index 993e3524b..eca028c9b 100644 --- a/perllib/FixMyStreet/DB/Result/Contact.pm +++ b/perllib/FixMyStreet/DB/Result/Contact.pm @@ -18,8 +18,8 @@ __PACKAGE__->add_columns( is_nullable => 0, sequence => "contacts_id_seq", }, - "area_id", - { data_type => "integer", is_nullable => 0 }, + "body_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, "category", { data_type => "text", default_value => "Other", is_nullable => 0 }, "email", @@ -48,30 +48,19 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); -__PACKAGE__->add_unique_constraint("contacts_area_id_category_idx", ["area_id", "category"]); +__PACKAGE__->add_unique_constraint("contacts_body_id_category_idx", ["body_id", "category"]); +__PACKAGE__->belongs_to( + "body", + "FixMyStreet::DB::Result::Body", + { id => "body_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-08-31 10:29:17 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t6yOPhZmedV/eH6AUvHI6w +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hq/BFHDEu4OUI4MSy3OyHg -__PACKAGE__->filter_column( - extra => { - filter_from_storage => sub { - my $self = shift; - my $ser = shift; - return undef unless defined $ser; - my $h = new IO::String($ser); - return RABX::wire_rd($h); - }, - filter_to_storage => sub { - my $self = shift; - my $data = shift; - my $ser = ''; - my $h = new IO::String($ser); - RABX::wire_wr( $data, $h ); - return $ser; - }, - } -); +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('extra'); 1; diff --git a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm index deb00fb95..7126d91c9 100644 --- a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm +++ b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm @@ -20,7 +20,7 @@ __PACKAGE__->add_columns( }, "contact_id", { data_type => "integer", is_nullable => 0 }, - "area_id", + "body_id", { data_type => "integer", is_nullable => 0 }, "category", { data_type => "text", default_value => "Other", is_nullable => 0 }, @@ -40,8 +40,8 @@ __PACKAGE__->add_columns( __PACKAGE__->set_primary_key("contacts_history_id"); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dN2ueIDoP3d/+Mg1UDqsMw +# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-12-12 16:37:16 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sxflEBBn0Mn0s3MroWnWFA # You can replace this text with custom code or comments, and it will be preserved on regeneration diff --git a/perllib/FixMyStreet/DB/Result/Open311conf.pm b/perllib/FixMyStreet/DB/Result/Open311conf.pm deleted file mode 100644 index 8051e27de..000000000 --- a/perllib/FixMyStreet/DB/Result/Open311conf.pm +++ /dev/null @@ -1,60 +0,0 @@ -use utf8; -package FixMyStreet::DB::Result::Open311conf; - -# 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("open311conf"); -__PACKAGE__->add_columns( - "id", - { - data_type => "integer", - is_auto_increment => 1, - is_nullable => 0, - sequence => "open311conf_id_seq", - }, - "area_id", - { data_type => "integer", is_nullable => 0 }, - "endpoint", - { data_type => "text", is_nullable => 0 }, - "jurisdiction", - { data_type => "text", is_nullable => 1 }, - "api_key", - { data_type => "text", is_nullable => 1 }, - "send_method", - { data_type => "text", is_nullable => 1 }, - "send_comments", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, - "comment_user_id", - { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, - "suppress_alerts", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, - "can_be_devolved", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, -); -__PACKAGE__->set_primary_key("id"); -__PACKAGE__->add_unique_constraint("open311conf_area_id_key", ["area_id"]); -__PACKAGE__->belongs_to( - "comment_user", - "FixMyStreet::DB::Result::User", - { id => "comment_user_id" }, - { - is_deferrable => 1, - join_type => "LEFT", - on_delete => "CASCADE", - on_update => "CASCADE", - }, -); - - -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-08-29 14:04:20 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Yoult8K/ldH6DMAKURtr3Q - - -# You can replace this text with custom code or comments, and it will be preserved on regeneration -1; diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index 02e5adb7d..3463ebab6 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -24,7 +24,7 @@ __PACKAGE__->add_columns( { data_type => "double precision", is_nullable => 0 }, "longitude", { data_type => "double precision", is_nullable => 0 }, - "council", + "bodies_str", { data_type => "text", is_nullable => 1 }, "areas", { data_type => "text", is_nullable => 0 }, @@ -99,7 +99,7 @@ __PACKAGE__->add_columns( "external_source_id", { data_type => "text", is_nullable => 1 }, "interest_count", - { data_type => "integer", is_nullable => 1 }, + { data_type => "integer", default_value => 0, is_nullable => 1 }, "subcategory", { data_type => "text", is_nullable => 1 }, ); @@ -120,12 +120,12 @@ __PACKAGE__->belongs_to( "user", "FixMyStreet::DB::Result::User", { id => "user_id" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-12-03 17:48:10 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xN/RB8Vx50CwyOeBjvJezQ +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:U/4BT8EGfcCLKA/7LX+qyQ # Add fake relationship to stored procedure table __PACKAGE__->has_one( @@ -135,52 +135,15 @@ __PACKAGE__->has_one( { cascade_copy => 0, cascade_delete => 0 }, ); -__PACKAGE__->filter_column( - extra => { - filter_from_storage => sub { - my $self = shift; - my $ser = shift; - return undef unless defined $ser; - my $h = new IO::String($ser); - return RABX::wire_rd($h); - }, - filter_to_storage => sub { - my $self = shift; - my $data = shift; - my $ser = ''; - my $h = new IO::String($ser); - RABX::wire_wr( $data, $h ); - return $ser; - }, - } -); - -__PACKAGE__->filter_column( - geocode => { - filter_from_storage => sub { - my $self = shift; - my $ser = shift; - return undef unless defined $ser; - my $h = new IO::String($ser); - return RABX::wire_rd($h); - }, - filter_to_storage => sub { - my $self = shift; - my $data = shift; - my $ser = ''; - my $h = new IO::String($ser); - RABX::wire_wr( $data, $h ); - return $ser; - }, - } -); +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('extra'); +__PACKAGE__->rabx_column('geocode'); use DateTime::TimeZone; use Image::Size; use Moose; use namespace::clean -except => [ 'meta' ]; use Utils; -use RABX; with 'FixMyStreet::Roles::Abuser'; @@ -196,10 +159,11 @@ HASHREF. sub open_states { my $states = { - 'confirmed' => 1, - 'investigating' => 1, - 'planned' => 1, - 'in progress' => 1, + 'confirmed' => 1, + 'investigating' => 1, + 'in progress' => 1, + 'planned' => 1, + 'action scheduled' => 1, }; return wantarray ? keys %{$states} : $states; @@ -237,7 +201,11 @@ HASHREF. sub closed_states { my $states = { - 'closed' => 1, + 'closed' => 1, + 'unable to fix' => 1, + 'not responsible' => 1, + 'duplicate' => 1, + 'internal referral' => 1, }; return wantarray ? keys %{$states} : $states; @@ -248,61 +216,111 @@ sub closed_states { @states = FixMyStreet::DB::Problem::visible_states(); -Get a list or states that should be visible on the site. If called in +Get a list of states that should be visible on the site. If called in array context then returns an array of names, otherwise returns a HASHREF. =cut +my $visible_states = { + '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 visible_states { + return wantarray ? keys %{$visible_states} : $visible_states; +} +sub visible_states_add_unconfirmed { + $visible_states->{unconfirmed} = 1; +} + +=head2 + + @states = FixMyStreet::DB::Problem::all_states(); + +Get a list of all states that a problem can have. If called in +array context then returns an array of names, otherwise returns a +HASHREF. + +=cut + +sub all_states { my $states = { - 'confirmed' => 1, - 'planned' => 1, - 'investigating' => 1, - 'in progress' => 1, - 'fixed' => 1, - 'fixed - council' => 1, - 'fixed - user' => 1, - 'closed' => 1, + '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, }; return wantarray ? keys %{$states} : $states; } +=head2 -my $tz = DateTime::TimeZone->new( name => "local" ); - -sub confirmed_local { - my $self = shift; + @states = FixMyStreet::DB::Problem::council_states(); - return $self->confirmed - ? $self->confirmed->set_time_zone($tz) - : $self->confirmed; -} +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. -sub created_local { - my $self = shift; +=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 $self->created - ? $self->created->set_time_zone($tz) - : $self->created; + return wantarray ? keys %{$states} : $states; } -sub whensent_local { - my $self = shift; +my $tz = DateTime::TimeZone->new( name => "local" ); - return $self->whensent - ? $self->whensent->set_time_zone($tz) - : $self->whensent; -} +my $tz_f; +$tz_f = DateTime::TimeZone->new( name => FixMyStreet->config('TIME_ZONE') ) + if FixMyStreet->config('TIME_ZONE'); -sub lastupdate_local { - my $self = shift; +my $stz = sub { + my ( $orig, $self ) = ( shift, shift ); + my $s = $self->$orig(@_); + return $s unless $s && UNIVERSAL::isa($s, "DateTime"); + $s->set_time_zone($tz); + $s->set_time_zone($tz_f) if $tz_f; + return $s; +}; - return $self->lastupdate - ? $self->lastupdate->set_time_zone($tz) - : $self->lastupdate; -} +around created => $stz; +around confirmed => $stz; +around whensent => $stz; +around lastupdate => $stz; around service => sub { my ( $orig, $self ) = ( shift, shift ); @@ -311,6 +329,12 @@ around service => sub { return $s; }; +sub title_safe { + my $self = shift; + return _('Awaiting moderation') if $self->cobrand eq 'zurich' && $self->state eq 'unconfirmed'; + return $self->title; +} + =head2 check_for_errors $error_hashref = $problem->check_for_errors(); @@ -335,9 +359,9 @@ sub check_for_errors { $errors{detail} = _('Please enter some details') unless $self->detail =~ m/\S/; - $errors{council} = _('No council selected') - unless $self->council - && $self->council =~ m/^(?:-1|[\d,]+(?:\|[\d,]+)?)$/; + $errors{bodies} = _('No council selected') + unless $self->bodies_str + && $self->bodies_str =~ m/^(?:-1|[\d,]+(?:\|[\d,]+)?)$/; if ( !$self->name || $self->name !~ m/\S/ ) { $errors{name} = _('Please enter your name'); @@ -348,7 +372,7 @@ sub check_for_errors { { $errors{name} = _( 'Please enter your full name, councils need this information – if you do not wish your name to be shown on the site, untick the box below' - ); + ) unless $self->cobrand eq 'emptyhomes'; } if ( $self->category @@ -364,6 +388,18 @@ sub check_for_errors { $self->category(undef); } + if ( $self->bodies_str && $self->detail ) { + # Custom character limit: + # Bromley Council + if ( $self->bodies_str eq '2482' && length($self->detail) > 1750 ) { + $errors{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), 1750 ); + } + # Oxfordshire + if ( $self->bodies_str eq '2237' && length($self->detail) > 1700 ) { + $errors{detail} = sprintf( _('Reports are limited to %s characters in length. Please shorten your report'), 1700 ); + } + } + return \%errors; } @@ -390,18 +426,26 @@ sub confirm { return 1; } -=head2 councils +sub bodies_str_ids { + my $self = shift; + return unless $self->bodies_str; + (my $bodies = $self->bodies_str) =~ s/\|.*$//; + my @bodies = split( /,/, $bodies ); + return \@bodies; +} -Returns an arrayref of councils to which a report was sent. +=head2 bodies + +Returns a hashref of bodies to which a report was sent. =cut -sub councils { +sub bodies($) { my $self = shift; - return [] unless $self->council; - (my $council = $self->council) =~ s/\|.*$//; - my @council = split( /,/, $council ); - return \@council; + return {} unless $self->bodies_str; + my $bodies = $self->bodies_str_ids; + my @bodies = FixMyStreet::App->model('DB::Body')->search({ id => $bodies })->all; + return { map { $_->id => $_ } @bodies }; } =head2 url @@ -485,8 +529,7 @@ meta data about the report. sub meta_line { my ( $problem, $c ) = @_; - my $date_time = - Utils::prettify_epoch( $problem->confirmed_local->epoch ); + my $date_time = Utils::prettify_dt( $problem->confirmed ); my $meta = ''; # FIXME Should be in cobrand @@ -494,11 +537,7 @@ sub meta_line { my $category = _($problem->category); utf8::decode($category); - if ($problem->anonymous) { - $meta = sprintf(_('%s, reported anonymously at %s'), $category, $date_time); - } else { - $meta = sprintf(_('%s, reported by %s at %s'), $category, $problem->name, $date_time); - } + $meta = sprintf(_('%s, reported at %s'), $category, $date_time); } else { @@ -507,11 +546,11 @@ sub meta_line { and $problem->category && $problem->category ne _('Other') ) { $meta = - sprintf( _('Reported by %s in the %s category anonymously at %s'), + sprintf( _('Reported via %s in the %s category anonymously at %s'), $problem->service, $problem->category, $date_time ); } elsif ( $problem->service ) { - $meta = sprintf( _('Reported by %s anonymously at %s'), + $meta = sprintf( _('Reported via %s anonymously at %s'), $problem->service, $date_time ); } elsif ( $problem->category and $problem->category ne _('Other') ) { @@ -527,13 +566,13 @@ sub meta_line { and $problem->category && $problem->category ne _('Other') ) { $meta = sprintf( - _('Reported by %s in the %s category by %s at %s'), + _('Reported via %s in the %s category by %s at %s'), $problem->service, $problem->category, $problem->name, $date_time ); } elsif ( $problem->service ) { - $meta = sprintf( _('Reported by %s by %s at %s'), + $meta = sprintf( _('Reported via %s by %s at %s'), $problem->service, $problem->name, $date_time ); } elsif ( $problem->category and $problem->category ne _('Other') ) { @@ -555,21 +594,22 @@ sub body { my ( $problem, $c ) = @_; my $body; if ($problem->external_body) { - $body = $problem->external_body; + if ($problem->cobrand eq 'zurich') { + $body = $c->model('DB::Body')->find({ id => $problem->external_body }); + } else { + $body = $problem->external_body; + } } else { - my $councils = $problem->councils; - my $areas_info = mySociety::MaPit::call('areas', $councils); + my $bodies = $problem->bodies; $body = join( _(' and '), map { - my $name = $areas_info->{$_}->{name}; + my $name = $_->name; if (mySociety::Config::get('AREA_LINKS_FROM_PROBLEMS')) { - '<a href="' - . $c->uri_for( '/reports/' . $c->cobrand->short_name( $areas_info->{$_} ) ) - . '">' . $name . '</a>'; + '<a href="' . $_->url($c) . '">' . $name . '</a>'; } else { $name; } - } @$councils + } values %$bodies ); } return $body; @@ -583,10 +623,10 @@ sub body { # 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->send_method_used eq 'barnet') { + if ($self->external_id && $self->bodies_str =~ /2237/) { return 1; } - return 0; + return 0; } # TODO Some/much of this could be moved to the template @@ -603,7 +643,7 @@ sub processed_summary_string { my ( $problem, $c ) = @_; my ($duration_clause, $external_ref_clause); if ($problem->whensent) { - $duration_clause = $problem->duration_string($c) + $duration_clause = $problem->duration_string($c); } if ($problem->can_display_external_id) { if ($duration_clause) { @@ -623,19 +663,30 @@ sub duration_string { my ( $problem, $c ) = @_; my $body = $problem->body( $c ); return sprintf(_('Sent to %s %s later'), $body, - Utils::prettify_duration($problem->whensent_local->epoch - $problem->confirmed_local->epoch, 'minute') + Utils::prettify_duration($problem->whensent->epoch - $problem->confirmed->epoch, 'minute') ); } +sub local_coords { + my $self = shift; + if ($self->cobrand eq 'zurich') { + my ($x, $y) = Geo::Coordinates::CH1903::from_latlon($self->latitude, $self->longitude); + return ( int($x+0.5), int($y+0.5) ); + } +} + =head2 update_from_open311_service_request - $p->update_from_open311_service_request( $request, $council_details, $system_user ); + $p->update_from_open311_service_request( $request, $body, $system_user ); -Updates the problem based on information in the passed in open311 request. If the request -has an older update time than the problem's lastupdate time then nothing happens. +Updates the problem based on information in the passed in open311 request +(standard, not the extension that uses GetServiceRequestUpdates) . If the +request has an older update time than the problem's lastupdate time then +nothing happens. -Otherwise a comment will be created if there is status update text in the open311 request. -If the open311 request has a state of closed then the problem will be marked as fixed. +Otherwise a comment will be created if there is status update text in the +open311 request. If the open311 request has a state of closed then the problem +will be marked as fixed. NB: a comment will always be created if the problem is being marked as fixed. @@ -644,7 +695,7 @@ Fixed problems will not be re-opened by this method. =cut sub update_from_open311_service_request { - my ( $self, $request, $council_details, $system_user ) = @_; + my ( $self, $request, $body, $system_user ) = @_; my ( $updated, $status_notes ); @@ -667,11 +718,10 @@ sub update_from_open311_service_request { mark_fixed => 0, user => $system_user, anonymous => 0, - name => $council_details->{name}, + name => $body->name, } ); - my $w3c = DateTime::Format::W3CDTF->new; my $req_time = $w3c->parse_datetime( $request->{updated_datetime} ); @@ -720,6 +770,29 @@ sub update_send_failed { } ); } +sub as_hashref { + my $self = shift; + my $c = shift; + + return { + id => $self->id, + title => $self->title, + category => $self->category, + detail => $self->detail, + latitude => $self->latitude, + longitude => $self->longitude, + postcode => $self->postcode, + state => $self->state, + state_t => _( $self->state ), + used_map => $self->used_map, + is_fixed => $self->fixed_states->{ $self->state } ? 1 : 0, + photo => $self->get_photo_params, + meta => $self->confirmed ? $self->meta_line( $c ) : '', + confirmed_pp => $self->confirmed ? $c->cobrand->prettify_dt( $self->confirmed ): '', + created_pp => $c->cobrand->prettify_dt( $self->created ), + }; +} + # we need the inline_constructor bit as we don't inherit from Moose __PACKAGE__->meta->make_immutable( inline_constructor => 0 ); diff --git a/perllib/FixMyStreet/DB/Result/Questionnaire.pm b/perllib/FixMyStreet/DB/Result/Questionnaire.pm index b6791603a..7f9c79d9a 100644 --- a/perllib/FixMyStreet/DB/Result/Questionnaire.pm +++ b/perllib/FixMyStreet/DB/Result/Questionnaire.pm @@ -36,31 +36,33 @@ __PACKAGE__->belongs_to( "problem", "FixMyStreet::DB::Result::Problem", { id => "problem_id" }, - { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:NGlSRjoBpDoIvK3EueqN6Q +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:oL1Hk4/bNG14CY74GA75SA use DateTime::TimeZone; +use Moose; +use namespace::clean -except => [ 'meta' ]; my $tz = DateTime::TimeZone->new( name => "local" ); -sub whensent_local { - my $self = shift; +my $tz_f; +$tz_f = DateTime::TimeZone->new( name => FixMyStreet->config('TIME_ZONE') ) + if FixMyStreet->config('TIME_ZONE'); - return $self->whensent - ? $self->whensent->set_time_zone($tz) - : $self->whensent; -} +my $stz = sub { + my ( $orig, $self ) = ( shift, shift ); + my $s = $self->$orig(@_); + return $s unless $s && UNIVERSAL::isa($s, "DateTime"); + $s->set_time_zone($tz); + $s->set_time_zone($tz_f) if $tz_f; + return $s; +}; -sub whenanswered_local { - my $self = shift; - - return $self->whenanswered - ? $self->whenanswered->set_time_zone($tz) - : $self->whenanswered; -} +around whensent => $stz; +around whenanswered => $stz; 1; diff --git a/perllib/FixMyStreet/DB/Result/Token.pm b/perllib/FixMyStreet/DB/Result/Token.pm index b223ada3a..5525fe7a5 100644 --- a/perllib/FixMyStreet/DB/Result/Token.pm +++ b/perllib/FixMyStreet/DB/Result/Token.pm @@ -34,8 +34,6 @@ __PACKAGE__->set_primary_key("scope", "token"); # use mySociety::DBHandle qw(dbh); use mySociety::AuthToken; -use IO::String; -use RABX; =head1 NAME @@ -54,25 +52,9 @@ ms_current_timestamp. =cut -__PACKAGE__->filter_column( - data => { - filter_from_storage => sub { - my $self = shift; - my $ser = shift; - return undef unless defined $ser; - my $h = new IO::String($ser); - return RABX::wire_rd($h); - }, - filter_to_storage => sub { - my $self = shift; - my $data = shift; - my $ser = ''; - my $h = new IO::String($ser); - RABX::wire_wr( $data, $h ); - return $ser; - }, - } -); +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('data'); + sub new { my ( $class, $attrs ) = @_; diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index 7f43d1a52..523382670 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -26,8 +26,8 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 1 }, "password", { data_type => "text", default_value => "", is_nullable => 0 }, - "from_council", - { data_type => "integer", is_nullable => 1 }, + "from_body", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, "flagged", { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "title", @@ -42,16 +42,27 @@ __PACKAGE__->has_many( { cascade_copy => 0, cascade_delete => 0 }, ); __PACKAGE__->has_many( + "bodies", + "FixMyStreet::DB::Result::Body", + { "foreign.comment_user_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); +__PACKAGE__->has_many( "comments", "FixMyStreet::DB::Result::Comment", { "foreign.user_id" => "self.id" }, { cascade_copy => 0, cascade_delete => 0 }, ); -__PACKAGE__->has_many( - "open311confs", - "FixMyStreet::DB::Result::Open311conf", - { "foreign.comment_user_id" => "self.id" }, - { cascade_copy => 0, cascade_delete => 0 }, +__PACKAGE__->belongs_to( + "from_body", + "FixMyStreet::DB::Result::Body", + { id => "from_body" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, ); __PACKAGE__->has_many( "problems", @@ -61,8 +72,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-05-01 16:20:29 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:LKi8u5IYnHW1+Mez64nvGg +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:jRAtXRLRNozCmthAg9p0dA __PACKAGE__->add_columns( "password" => { @@ -144,38 +155,27 @@ sub alert_for_problem { } ); } -sub council { +sub body { my $self = shift; - - return '' unless $self->from_council; - - my $key = 'council_name:' . $self->from_council; - my $result = Memcached::get($key); - - unless ($result) { - my $area_info = mySociety::MaPit::call('area', $self->from_council); - $result = $area_info->{name}; - Memcached::set($key, $result, 86400); - } - - return $result; + return '' unless $self->from_body; + return $self->from_body->name; } -=head2 belongs_to_council +=head2 belongs_to_body - $belongs_to_council = $user->belongs_to_council( $council_list ); + $belongs_to_body = $user->belongs_to_body( $bodies ); -Returns true if the user belongs to the comma seperated list of council ids passed in +Returns true if the user belongs to the comma seperated list of body ids passed in =cut -sub belongs_to_council { +sub belongs_to_body { my $self = shift; - my $council = shift; + my $bodies = shift; - my %councils = map { $_ => 1 } split ',', $council; + my %bodies = map { $_ => 1 } split ',', $bodies; - return 1 if $self->from_council && $councils{ $self->from_council }; + return 1 if $self->from_body && $bodies{ $self->from_body->id }; return 0; } diff --git a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm index 468df2654..545b54c60 100644 --- a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm +++ b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm @@ -29,6 +29,7 @@ sub email_alerts ($) { $query .= " $item_table.id as item_id, $item_table.text as item_text, $item_table.name as item_name, $item_table.anonymous as item_anonymous, + $item_table.confirmed as item_confirmed, $head_table.* from alert inner join $item_table on alert.parameter::integer = $item_table.${head_table}_id @@ -58,6 +59,7 @@ sub email_alerts ($) { while (my $row = $query->fetchrow_hashref) { my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->{alert_cobrand})->new(); + $cobrand->set_lang_and_domain( $row->{alert_lang}, 1, FixMyStreet->path_to('locale')->stringify ); # Cobranded and non-cobranded messages can share a database. In this case, the conf file # should specify a vhost to send the reports for each cobrand, so that they don't get sent @@ -89,13 +91,14 @@ sub email_alerts ($) { } my $url = $cobrand->base_url( $row->{alert_cobrand_data} ); - if ( $hashref_restriction && $hashref_restriction->{council} && $row->{council} ne $hashref_restriction->{council} ) { + if ( $hashref_restriction && $hashref_restriction->{bodies_str} && $row->{bodies_str} ne $hashref_restriction->{bodies_str} ) { $url = mySociety::Config::get('BASE_URL'); } # this is currently only for new_updates if ($row->{item_text}) { - if ( $row->{alert_user_id} == $row->{user_id} ) { + 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 my $user = FixMyStreet::App->model('DB::User')->find( { id => $row->{alert_user_id} } ); @@ -112,6 +115,27 @@ sub email_alerts ($) { $data{problem_url} = $url . "/report/" . $row->{id}; } $data{data} .= $row->{item_name} . ' : ' if $row->{item_name} && !$row->{item_anonymous}; + if ( $cobrand->include_time_in_update_alerts ) { + # this is basically recreating the code from the inflate wrapper + # in the database model. + my $tz; + if ( FixMyStreet->config('TIME_ZONE') ) { + $tz = FixMyStreet->config('TIME_ZONE'); + } + + my $parser = DateTime::Format::Pg->new(); + my $dt = $parser->parse_timestamp( $row->{item_confirmed} ); + my $l_tz = DateTime::TimeZone->new( name => "local" ); + # We need to always set this otherwise we end up with the DateTime + # object being in the floating timezone in which case applying a + # subsequent timezone set will have no effect. + $dt->set_time_zone( $l_tz ); + if ( $tz ) { + my $tz_obj = DateTime::TimeZone->new( name => $tz ); + $dt->set_time_zone( $tz_obj ); + } + $data{data} .= $cobrand->prettify_dt( $dt, 'alert' ) . "\n\n"; + } $data{data} .= $row->{item_text} . "\n\n------\n\n"; # this is ward and council problems } else { @@ -155,6 +179,7 @@ sub email_alerts ($) { while (my $alert = $query->next) { my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($alert->cobrand)->new(); next unless $cobrand->email_host; + next if $alert->is_from_abuser; my $longitude = $alert->parameter; my $latitude = $alert->parameter2; @@ -166,7 +191,7 @@ sub email_alerts ($) { }; my $states = "'" . join( "', '", FixMyStreet::DB::Result::Problem::visible_states() ) . "'"; my %data = ( template => $template, data => '', alert_id => $alert->id, alert_email => $alert->user->email, lang => $alert->lang, cobrand => $alert->cobrand, cobrand_data => $alert->cobrand_data ); - my $q = "select problem.id, problem.council, problem.postcode, problem.geocode, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users + my $q = "select problem.id, problem.bodies_str, problem.postcode, problem.geocode, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users where nearby.problem_id = problem.id and problem.user_id = users.id and problem.state in ($states) @@ -183,7 +208,7 @@ sub email_alerts ($) { parameter => $row->{id}, } ); my $url = $cobrand->base_url( $alert->cobrand_data ); - if ( $hashref_restriction && $hashref_restriction->{council} && $row->{council} ne $hashref_restriction->{council} ) { + if ( $hashref_restriction && $hashref_restriction->{bodies_str} && $row->{bodies_str} ne $hashref_restriction->{bodies_str} ) { $url = mySociety::Config::get('BASE_URL'); } $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; @@ -202,7 +227,7 @@ sub _send_aggregated_alert_email(%) { my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($data{cobrand})->new(); - $cobrand->set_lang_and_domain( $data{lang}, 1 ); + $cobrand->set_lang_and_domain( $data{lang}, 1, FixMyStreet->path_to('locale')->stringify ); if (!$data{alert_email}) { my $user = FixMyStreet::App->model('DB::User')->find( { @@ -211,6 +236,11 @@ sub _send_aggregated_alert_email(%) { $data{alert_email} = $user->email; } + my ($domain) = $data{alert_email} =~ m{ @ (.*) \z }x; + return if FixMyStreet::App->model('DB::Abuse')->search( { + email => [ $data{alert_email}, $domain ] + } )->first; + my $token = FixMyStreet::App->model("DB::Token")->new_result( { scope => 'alert', data => { @@ -230,13 +260,12 @@ sub _send_aggregated_alert_email(%) { unless -e $template; $template = Utils::read_file($template); - my $sender = $cobrand->contact_email; - (my $from = $sender) =~ s/team/fms-DO-NOT-REPLY/; # XXX + my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL'); my $result = FixMyStreet::App->send_email_cron( { _template_ => $template, _parameters_ => \%data, - From => [ $from, _($cobrand->contact_name) ], + From => [ $sender, _($cobrand->contact_name) ], To => $data{alert_email}, }, $sender, @@ -255,6 +284,7 @@ sub _get_address_from_gecode { my $geocode = shift; return '' unless defined $geocode; + utf8::encode($geocode) if utf8::is_utf8($geocode); my $h = new IO::String($geocode); my $data = RABX::wire_rd($h); diff --git a/perllib/FixMyStreet/DB/ResultSet/Body.pm b/perllib/FixMyStreet/DB/ResultSet/Body.pm new file mode 100644 index 000000000..6802ed604 --- /dev/null +++ b/perllib/FixMyStreet/DB/ResultSet/Body.pm @@ -0,0 +1,17 @@ +package FixMyStreet::DB::ResultSet::Body; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings; + +sub for_areas { + my ( $rs, @areas ) = @_; + + my $result = $rs->search( + { 'body_areas.area_id' => \@areas }, + { join => 'body_areas' } + ); + return $result; +} + +1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm index 191223572..91c44d5f4 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm @@ -21,12 +21,7 @@ sub nearby { } if $c->cobrand->problems_clause; my $attrs = { - join => 'problem', - columns => [ - 'problem.id', 'problem.title', 'problem.latitude', - 'problem.longitude', 'distance', 'problem.state', - 'problem.confirmed', { 'problem.photo' => 'problem.photo is not null' }, - ], + prefetch => 'problem', bind => [ $mid_lat, $mid_lon, $dist ], order_by => [ 'distance', { -desc => 'created' } ], rows => $limit, diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm index 6da383d6c..c108f7e29 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm @@ -84,15 +84,16 @@ sub _recent { my $key = $photos ? 'recent_photos' : 'recent'; $key .= ":$site_key:$num"; + # unconfirmed might be returned for e.g. Zurich, but would mean in moderation, so no photo + my @states = grep { $_ ne 'unconfirmed' } FixMyStreet::DB::Result::Problem->visible_states(); my $query = { non_public => 0, - state => [ FixMyStreet::DB::Result::Problem->visible_states() ], + state => \@states, }; $query->{photo} = { '!=', undef } if $photos; my $attrs = { - columns => [ 'id', 'title', 'confirmed' ], - order_by => { -desc => 'confirmed' }, + order_by => { -desc => 'coalesce(confirmed, created)' }, rows => $num, }; @@ -134,10 +135,6 @@ sub around_map { my ( $rs, $min_lat, $max_lat, $min_lon, $max_lon, $interval, $limit ) = @_; my $attr = { order_by => { -desc => 'created' }, - columns => [ - 'id', 'title', 'latitude', 'longitude', 'state', 'confirmed', - { photo => 'photo is not null' }, - ], }; $attr->{rows} = $limit if $limit; @@ -159,7 +156,7 @@ sub around_map { sub timeline { my ( $rs ) = @_; - my $prefetch = + my $prefetch = FixMyStreet::App->model('DB')->schema->storage->sql_maker->quote_char ? [ qw/user/ ] : []; @@ -219,46 +216,74 @@ sub categories_summary { return \%categories; } +sub get_admin_url { + my ($rs, $cobrand, $row) = @_; + return $cobrand->admin_base_url . '/report_edit/' . $row->id; +} + sub send_reports { + my ( $rs, $site_override ) = @_; + # Set up site, language etc. - my ($verbose, $nomail) = CronFns::options(); + my ($verbose, $nomail, $debug_mode) = CronFns::options(); + my $base_url = mySociety::Config::get('BASE_URL'); - my $site = CronFns::site($base_url); + my $site = $site_override || CronFns::site($base_url); + my $states = [ 'confirmed', 'fixed' ]; + $states = [ 'unconfirmed', 'confirmed', 'in progress', 'planned', 'closed' ] if $site eq 'zurich'; my $unsent = FixMyStreet::App->model("DB::Problem")->search( { - state => [ 'confirmed', 'fixed' ], + state => $states, whensent => undef, - council => { '!=', undef }, + bodies_str => { '!=', undef }, } ); my (%notgot, %note); my $send_report = FixMyStreet::SendReport->new(); my $senders = $send_report->get_senders; - my %sending_skipped_by_method; + my $debug_unsent_count = 0; + debug_print("starting to loop through unsent problem reports...") if $debug_mode; while (my $row = $unsent->next) { my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new(); - # Cobranded and non-cobranded messages can share a database. In this case, the conf file - # should specify a vhost to send the reports for each cobrand, so that they don't get sent + if ($debug_mode) { + $debug_unsent_count++; + print "\n"; + debug_print("state=" . $row->state . ", bodies_str=" . $row->bodies_str . ($row->cobrand? ", cobrand=" . $row->cobrand : ""), $row->id); + } + + # Cobranded and non-cobranded messages can share a database. In this case, the conf file + # should specify a vhost to send the reports for each cobrand, so that they don't get sent # more than once if there are multiple vhosts running off the same database. The email_host # call checks if this is the host that sends mail for this cobrand. - next unless $cobrand->email_host(); + if (! $cobrand->email_host()) { + debug_print("skipping because this host does not send reports for cobrand " . $cobrand->moniker, $row->id) if $debug_mode; + next; + } + $cobrand->set_lang_and_domain($row->lang, 1); - if ( $row->is_from_abuser ) { + if ( $row->is_from_abuser) { + $row->update( { state => 'hidden' } ); + debug_print("hiding because its sender is flagged as an abuser", $row->id) if $debug_mode; + next; + } elsif ( $row->title =~ /app store test/i ) { $row->update( { state => 'hidden' } ); + debug_print("hiding because it is an app store test message", $row->id) if $debug_mode; next; } # Template variables for the email my $email_base_url = $cobrand->base_url_for_report($row); my %h = map { $_ => $row->$_ } qw/id title detail name category latitude longitude used_map/; - map { $h{$_} = $row->user->$_ } qw/email phone/; - $h{confirmed} = DateTime::Format::Pg->format_datetime( $row->confirmed->truncate (to => 'second' ) ); + map { $h{$_} = $row->user->$_ || '' } qw/email phone/; + $h{confirmed} = DateTime::Format::Pg->format_datetime( $row->confirmed->truncate (to => 'second' ) ) + if $row->confirmed; $h{query} = $row->postcode; $h{url} = $email_base_url . $row->url; + $h{admin_url} = $rs->get_admin_url($cobrand, $row); $h{phone_line} = $h{phone} ? _('Phone:') . " $h{phone}\n\n" : ''; if ($row->photo) { $h{has_photo} = _("This web page also contains a photo of the problem, provided by the user.") . "\n\n"; @@ -302,39 +327,42 @@ sub send_reports { my ( $sender_count ); if ($site eq 'emptyhomes') { - my $council = $row->council; - my $areas_info = mySociety::MaPit::call('areas', $council); + my $body = $row->bodies_str; + $body = FixMyStreet::App->model("DB::Body")->find($body); my $sender = "FixMyStreet::SendReport::EmptyHomes"; $reporters{ $sender } = $sender->new() unless $reporters{$sender}; - $reporters{ $sender }->add_council( $council, $areas_info->{$council} ); + $reporters{ $sender }->add_body( $body ); + $sender_count = 1; } else { # XXX Needs locks! - my @all_councils = split /,|\|/, $row->council; - my ($councils, $missing) = $row->council =~ /^([\d,]+)(?:\|([\d,]+))?/; - my @councils = split(/,/, $councils); - my $areas_info = mySociety::MaPit::call('areas', \@all_councils); + # XXX Only copes with at most one missing body + my ($bodies, $missing) = $row->bodies_str =~ /^([\d,]+)(?:\|(\d+))?/; + my @bodies = split(/,/, $bodies); + $bodies = FixMyStreet::App->model("DB::Body")->search( + { id => \@bodies }, + { order_by => 'name' }, + ); + $missing = FixMyStreet::App->model("DB::Body")->find($missing) if $missing; my @dear; - foreach my $council (@councils) { - my $name = $areas_info->{$council}->{name}; - - my $sender_info = $cobrand->get_council_sender( $council, $areas_info->{$council}, $row->category ); + while (my $body = $bodies->next) { + my $sender_info = $cobrand->get_body_sender( $body, $row->category ); my $sender = "FixMyStreet::SendReport::" . $sender_info->{method}; if ( ! exists $senders->{ $sender } ) { - warn "No such sender [ $sender ] for council $name ( $council )"; + warn "No such sender [ $sender ] for body $body->name ( $body->id )"; next; } $reporters{ $sender } ||= $sender->new(); if ( $reporters{ $sender }->should_skip( $row ) ) { - $sending_skipped_by_method{ $sender }++ if - $reporters{ $sender }->skipped; + debug_print("skipped by sender " . $sender_info->{method} . " (might be due to previous failed attempts?)", $row->id) if $debug_mode; } else { - push @dear, $name; - $reporters{ $sender }->add_council( $council, $areas_info->{$council}, $sender_info->{config} ); + debug_print("OK, adding recipient body " . $body->id . ":" . $body->name . ", " . $body->send_method, $row->id) if $debug_mode; + push @dear, $body->name; + $reporters{ $sender }->add_body( $body, $sender_info->{config} ); } } @@ -352,7 +380,7 @@ sub send_reports { $h{subcategory_line} = "\n\n"; } - $h{councils_name} = join(_(' and '), @dear); + $h{bodies_name} = join(_(' and '), @dear); if ($h{category} eq _('Other')) { $h{multiple} = @dear>1 ? "[ " . _("This email has been sent to both councils covering the location of the problem, as the user did not categorise it; please ignore it if you're not the correct council to deal with the issue, or let us know what category of problem this is so we can add it to our system.") . " ]\n\n" : ''; @@ -360,11 +388,10 @@ sub send_reports { $h{multiple} = @dear>1 ? "[ " . _("This email has been sent to several councils covering the location of the problem, as the category selected is provided for all of them; please ignore it if you're not the correct council to deal with the issue.") . " ]\n\n" : ''; } - $h{missing} = ''; + $h{missing} = ''; if ($missing) { - my $name = $areas_info->{$missing}->{name}; $h{missing} = '[ ' - . sprintf(_('We realise this problem might be the responsibility of %s; however, we don\'t currently have any contact details for them. If you know of an appropriate contact address, please do get in touch.'), $name) + . sprintf(_('We realise this problem might be the responsibility of %s; however, we don\'t currently have any contact details for them. If you know of an appropriate contact address, please do get in touch.'), $missing->name) . " ]\n\n"; } @@ -375,16 +402,16 @@ sub send_reports { die 'Report not going anywhere for ID ' . $row->id . '!'; } - next unless $sender_count; + if (! $sender_count) { + debug_print("can't send because sender count is zero", $row->id) if $debug_mode; + next; + } - if (mySociety::Config::get('STAGING_SITE')) { - # on a staging server send emails to ourselves rather than the councils - my @testing_councils = split( '\|', mySociety::Config::get('TESTING_COUNCILS') ); - unless ( grep { $row->council eq $_ } @testing_councils ) { - %reporters = map { $_ => $reporters{$_} } grep { /FixMyStreet::SendReport::(Email|NI)/ } keys %reporters; - unless (%reporters) { - %reporters = ( 'FixMyStreet::SendReport::Email' => FixMyStreet::SendReport::Email->new() ); - } + if (mySociety::Config::get('STAGING_SITE') && !mySociety::Config::get('SEND_REPORTS_ON_STAGING')) { + # on a staging server send emails to ourselves rather than the bodies + %reporters = map { $_ => $reporters{$_} } grep { /FixMyStreet::SendReport::(Email|NI|EmptyHomes)/ } keys %reporters; + unless (%reporters) { + %reporters = ( 'FixMyStreet::SendReport::Email' => FixMyStreet::SendReport::Email->new() ); } } @@ -392,6 +419,7 @@ sub send_reports { my $result = -1; for my $sender ( keys %reporters ) { + debug_print("sending using " . $sender, $row->id) if $debug_mode; $result *= $reporters{ $sender }->send( $row, \%h ); if ( $reporters{ $sender }->unconfirmed_counts) { foreach my $e (keys %{ $reporters{ $sender }->unconfirmed_counts } ) { @@ -414,6 +442,7 @@ sub send_reports { if ( $cobrand->report_sent_confirmation_email && !$h{anonymous_report}) { _send_report_sent_email( $row, \%h, $nomail ); } + debug_print("send successful: OK", $row->id) if $debug_mode; } else { my @errors; for my $sender ( keys %reporters ) { @@ -422,34 +451,35 @@ sub send_reports { } } $row->update_send_failed( join( '|', @errors ) ); + debug_print("send FAILED: " . join( '|', @errors ), $row->id) if $debug_mode; + } + } + if ($debug_mode) { + print "\n"; + if ($debug_unsent_count) { + debug_print("processed all unsent reports (total: $debug_unsent_count)"); + } else { + debug_print("no unsent reports were found (must have whensent=null and suitable bodies_str & state) -- nothing to send"); } } - if ($verbose) { + if ($verbose || $debug_mode) { print "Council email addresses that need checking:\n" if keys %notgot; foreach my $e (keys %notgot) { foreach my $c (keys %{$notgot{$e}}) { print " " . $notgot{$e}{$c} . " problem, to $e category $c (" . $note{$e}{$c}. ")\n"; } } - if (keys %sending_skipped_by_method) { - my $c = 0; - print "\nProblem reports that send-reports did not attempt to send the following:\n"; - foreach my $send_method (sort keys %sending_skipped_by_method) { - printf " %-24s %4d\n", "$send_method:", $sending_skipped_by_method{$send_method}; - $c+=$sending_skipped_by_method{$send_method}; - } - printf " %-24s %4d\n", "Total:", $c; - } my $sending_errors = ''; my $unsent = FixMyStreet::App->model("DB::Problem")->search( { state => [ 'confirmed', 'fixed' ], whensent => undef, - council => { '!=', undef }, + bodies_str => { '!=', undef }, send_fail_count => { '>', 0 } } ); while (my $row = $unsent->next) { - $sending_errors .= "* http://www.fixmystreet.com/report/" . $row->id . ", failed " + my $base_url = mySociety::Config::get('BASE_URL'); + $sending_errors .= "* " . $base_url . "/report/" . $row->id . ", failed " . $row->send_fail_count . " times, last at " . $row->send_fail_timestamp . ", reason " . $row->send_fail_reason . "\n"; } @@ -485,4 +515,11 @@ sub _send_report_sent_email { ); } +sub debug_print { + my $msg = shift; + my $id = shift || ''; + $id = "report $id: " if $id; + print "[] $id$msg\n"; +} + 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm b/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm index d6b3eb5cb..6f2c19b5e 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm @@ -50,6 +50,7 @@ sub send_questionnaires_period { # Not all cobrands send questionnaires next unless $cobrand->send_questionnaires; + next if $row->is_from_abuser; # Cobranded and non-cobranded messages can share a database. In this case, the conf file # should specify a vhost to send the reports for each cobrand, so that they don't get sent @@ -89,9 +90,8 @@ sub send_questionnaires_period { } ); $h{url} = $cobrand->base_url($row->cobrand_data) . '/Q/' . $token->token; - my $sender = $cobrand->contact_email; + my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL'); my $sender_name = _($cobrand->contact_name); - $sender =~ s/team/fms-DO-NOT-REPLY/; print "Sending questionnaire " . $questionnaire->id . ", problem " . $row->id . ", token " . $token->token . " to " diff --git a/perllib/FixMyStreet/EmailSend.pm b/perllib/FixMyStreet/EmailSend.pm index 61d8a70c2..8b6eed462 100644 --- a/perllib/FixMyStreet/EmailSend.pm +++ b/perllib/FixMyStreet/EmailSend.pm @@ -2,10 +2,7 @@ package FixMyStreet::EmailSend; use base Email::Send::SMTP; sub get_env_sender { - # Should really use cobrand's contact_email function, but not sure how - # best to access that from in here. - my $sender = FixMyStreet->config('CONTACT_EMAIL'); - $sender =~ s/team/fms-DO-NOT-REPLY/; + my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL'); return $sender; } diff --git a/perllib/FixMyStreet/Geocode.pm b/perllib/FixMyStreet/Geocode.pm index f92e9cc9a..61c398985 100644 --- a/perllib/FixMyStreet/Geocode.pm +++ b/perllib/FixMyStreet/Geocode.pm @@ -13,6 +13,7 @@ use URI::Escape; use FixMyStreet::Geocode::Bing; use FixMyStreet::Geocode::Google; use FixMyStreet::Geocode::OSM; +use FixMyStreet::Geocode::Zurich; # lookup STRING CONTEXT # Given a user-inputted string, try and convert it into co-ordinates using either @@ -30,21 +31,30 @@ sub lookup { } # string STRING CONTEXT -# Canonicalises, and then passes to some external API to look stuff up. +# Passes the string to some external API to look stuff up. sub string { my ($s, $c) = @_; + + my $service = $c->config->{GEOCODER}; + $service = $service->{type} if ref $service; + $service = 'OSM' unless $service =~ /^(Bing|Google|OSM|Zurich)$/; + $service = 'OSM' if $service eq 'Bing' && !FixMyStreet->config('BING_MAPS_API_KEY'); + $service = "FixMyStreet::Geocode::${service}::string"; + + no strict 'refs'; + return &$service($s, $c); +} + +# escape STRING CONTEXT +# Escapes string for putting in URL geocoding call +sub escape { + my ($s, $c) = @_; $s = lc($s); $s =~ s/[^-&\w ']/ /g; $s =~ s/\s+/ /g; $s = URI::Escape::uri_escape_utf8($s); $s =~ s/%20/+/g; - my $params = $c->cobrand->disambiguate_location($s); - return FixMyStreet::Geocode::Bing::string($s, $c, $params) - if FixMyStreet->config('BING_MAPS_API_KEY'); - # Fall back to Google API, which allow access with and without a key - return FixMyStreet::Geocode::Google::string($s, $c, $params) - if FixMyStreet->config('GOOGLE_MAPS_API_KEY'); - return FixMyStreet::Geocode::OSM::string($s, $c, $params); + return $s; } 1; diff --git a/perllib/FixMyStreet/Geocode/Bing.pm b/perllib/FixMyStreet/Geocode/Bing.pm index 18e6b56ce..85eef3d0f 100644 --- a/perllib/FixMyStreet/Geocode/Bing.pm +++ b/perllib/FixMyStreet/Geocode/Bing.pm @@ -15,14 +15,21 @@ use File::Path (); use LWP::Simple; use Digest::MD5 qw(md5_hex); +use mySociety::Locale; + # string STRING CONTEXT # Looks up on Bing Maps API, and caches, a user-inputted location. # Returns array of (LAT, LON, ERROR), where ERROR is either undef, a string, or # an array of matches if there are more than one. The information in the query # may be used to disambiguate the location in cobranded versions of the site. sub string { - my ( $s, $c, $params ) = @_; + my ( $s, $c ) = @_; + + my $params = $c->cobrand->disambiguate_location($s); + + $s = FixMyStreet::Geocode::escape($s); $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; + my $url = "http://dev.virtualearth.net/REST/v1/Locations?q=$s"; $url .= '&userMapView=' . join(',', @{$params->{bounds}}) if $params->{bounds}; diff --git a/perllib/FixMyStreet/Geocode/FixaMinGata.pm b/perllib/FixMyStreet/Geocode/FixaMinGata.pm new file mode 100644 index 000000000..2db25f504 --- /dev/null +++ b/perllib/FixMyStreet/Geocode/FixaMinGata.pm @@ -0,0 +1,204 @@ +#!/usr/bin/perl +# +# FixMyStreet:Geocode::FixaMinGata +# OpenStreetmap forward and reverse geocoding for FixMyStreet. +# +# Copyright (c) 2011 Petter Reinholdtsen. Some rights reserved. +# Email: pere@hungry.com + +# This module is a slightly derived version of OSM.pm. + +# As of January 2014, the FixaMinGata developers are considering to make further +# changes related to OSM, so it's probably best to keep this module separate +# from the OSM module for now. + +package FixMyStreet::Geocode::FixaMinGata; + +use warnings; +use strict; +use Data::Dumper; + +use Digest::MD5 qw(md5_hex); +use Encode; +use File::Slurp; +use File::Path (); +use LWP::Simple qw($ua); +use Memcached; +use XML::Simple; +use mySociety::Locale; + +my $osmapibase = "http://www.openstreetmap.org/api/"; +my $nominatimbase = "http://nominatim.openstreetmap.org/"; + +# string STRING CONTEXT +# Looks up on Nominatim, and caches, a user-inputted location. +# Returns array of (LAT, LON, ERROR), where ERROR is either undef, a string, or +# an array of matches if there are more than one. The information in the query +# may be used to disambiguate the location in cobranded versions of the site. +sub string { + my ( $s, $c ) = @_; + + my $params = $c->cobrand->disambiguate_location($s); + + $s = FixMyStreet::Geocode::escape($s); + # $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; + + my $url = "${nominatimbase}search?"; + my %query_params = ( + q => $s, + format => 'json', + addressdetails => 1, + limit => 20, + #'accept-language' => '', + email => 'info' . chr(64) . 'morus.se', + ); + # $query_params{viewbox} = $params->{bounds}[1] . ',' . $params->{bounds}[2] . ',' . $params->{bounds}[3] . ',' . $params->{bounds}[0] + # if $params->{bounds}; + $query_params{countrycodes} = $params->{country} + if $params->{country}; + $url .= join('&', map { "$_=$query_params{$_}" } keys %query_params); + + my $cache_dir = FixMyStreet->config('GEO_CACHE') . 'osm/'; + my $cache_file = $cache_dir . md5_hex($url); + my $js; + if (-s $cache_file) { + $js = File::Slurp::read_file($cache_file); + } else { + $ua->timeout(15); + $js = LWP::Simple::get($url); + $js = encode_utf8($js) if utf8::is_utf8($js); + File::Path::mkpath($cache_dir); + File::Slurp::write_file($cache_file, $js) if $js; + } + + if (!$js) { + return { error => _('Sorry, we could not find that location.') }; + } + + $js = JSON->new->utf8->allow_nonref->decode($js); + + my ( %locations, $error, @valid_locations, $latitude, $longitude ); + foreach (@$js) { + # These co-ordinates are output as query parameters in a URL, make sure they have a "." + next if $_->{class} eq "boundary"; + + my @s = split(/,/, $_->{display_name}); + + my $address = join(",", @s[0,1,2]); + + $locations{$address} = [$_->{lat}, $_->{lon}]; + } + + my ($key) = keys %locations; + + return { latitude => $locations{$key}[0], longitude => $locations{$key}[1] } if scalar keys %locations == 1; + return { error => _('Sorry, we could not find that location.') } if scalar keys %locations == 0; + + foreach $key (keys %locations) { + ( $latitude, $longitude ) = ($locations{$key}[0], $locations{$key}[1]); + mySociety::Locale::in_gb_locale { + push (@$error, { + address => $key, + latitude => sprintf('%0.6f', $latitude), + longitude => sprintf('%0.6f', $longitude) + }); + }; + } + + return { error => $error }; +} + +sub reverse_geocode { + my ($latitude, $longitude, $zoom) = @_; + my $url = + "${nominatimbase}reverse?format=xml&zoom=$zoom&lat=$latitude&lon=$longitude"; + my $key = "OSM:reverse_geocode:$url"; + my $result = Memcached::get($key); + unless ($result) { + my $j = LWP::Simple::get($url); + if ($j) { + Memcached::set($key, $j, 3600); + my $ref = XMLin($j); + return $ref; + } else { + print STDERR "No reply from $url\n"; + } + return undef; + } + return XMLin($result); +} + +sub _osmxml_to_hash { + my ($xml, $type) = @_; + my $ref = XMLin($xml); + my %tags; + if ('ARRAY' eq ref $ref->{$type}->{tag}) { + map { $tags{$_->{'k'}} = $_->{'v'} } @{$ref->{$type}->{tag}}; + return \%tags; + } else { + return undef; + } +} + +sub get_object_tags { + my ($type, $id) = @_; + my $url = "${osmapibase}0.6/$type/$id"; + my $key = "OSM:get_object_tags:$url"; + my $result = Memcached::get($key); + unless ($result) { + my $j = LWP::Simple::get($url); + if ($j) { + Memcached::set($key, $j, 3600); + return _osmxml_to_hash($j, $type); + } else { + print STDERR "No reply from $url\n"; + } + return undef; + } + return _osmxml_to_hash($result, $type); +} + +# A better alternative might be +# http://www.geonames.org/maps/osm-reverse-geocoder.html#findNearbyStreetsOSM +sub get_nearest_road_tags { + my ( $cobrand, $latitude, $longitude ) = @_; + my $inforef = reverse_geocode($latitude, $longitude, 16); + if (exists $inforef->{result}->{osm_type} + && 'way' eq $inforef->{result}->{osm_type}) { + my $osmtags = get_object_tags('way', + $inforef->{result}->{osm_id}); + unless ( exists $osmtags->{operator} ) { + $osmtags->{operatorguess} = $cobrand->guess_road_operator( $osmtags ); + } + return $osmtags; + } + return undef; +} + +sub closest_road_text { + my ( $cobrand, $latitude, $longitude ) = @_; + my $str = ''; + my $osmtags = get_nearest_road_tags( $cobrand, $latitude, $longitude ); + if ($osmtags) { + my ($name, $ref) = ('',''); + $name = $osmtags->{name} if exists $osmtags->{name}; + $ref = " ($osmtags->{ref})" if exists $osmtags->{ref}; + if ($name || $ref) { + $str .= _('The following information about the nearest road might be inaccurate or irrelevant, if the problem is close to several roads or close to a road without a name registered in OpenStreetMap.') . "\n\n"; + $str .= sprintf(_("Nearest named road to the pin placed on the map (automatically generated using OpenStreetMap): %s%s"), + $name, $ref) . "\n\n"; + + if (my $operator = $osmtags->{operator}) { + $str .= sprintf(_("Road operator for this named road (from OpenStreetMap): %s"), + $operator) . "\n\n"; + } elsif ($operator = $osmtags->{operatorguess}) { + $str .= sprintf(_("Road operator for this named road (derived from road reference number and type): %s"), + $operator) . "\n\n"; + } + } + } + return $str; +} + +1; + diff --git a/perllib/FixMyStreet/Geocode/Google.pm b/perllib/FixMyStreet/Geocode/Google.pm index db3a8ae91..fd65b89b1 100644 --- a/perllib/FixMyStreet/Geocode/Google.pm +++ b/perllib/FixMyStreet/Geocode/Google.pm @@ -14,6 +14,7 @@ use File::Slurp; use File::Path (); use LWP::Simple; use Digest::MD5 qw(md5_hex); +use mySociety::Locale; # string STRING CONTEXT # Looks up on Google Maps API, and caches, a user-inputted location. @@ -21,7 +22,11 @@ use Digest::MD5 qw(md5_hex); # an array of matches if there are more than one. The information in the query # may be used to disambiguate the location in cobranded versions of the site. sub string { - my ( $s, $c, $params ) = @_; + my ( $s, $c ) = @_; + + my $params = $c->cobrand->disambiguate_location($s); + + $s = FixMyStreet::Geocode::escape($s); my $url = 'http://maps.google.com/maps/geo?q=' . $s; $url .= '&ll=' . $params->{centre} if $params->{centre}; diff --git a/perllib/FixMyStreet/Geocode/OSM.pm b/perllib/FixMyStreet/Geocode/OSM.pm index d96338c16..fd14b0acc 100644 --- a/perllib/FixMyStreet/Geocode/OSM.pm +++ b/perllib/FixMyStreet/Geocode/OSM.pm @@ -29,8 +29,13 @@ my $nominatimbase = "http://nominatim.openstreetmap.org/"; # an array of matches if there are more than one. The information in the query # may be used to disambiguate the location in cobranded versions of the site. sub string { - my ( $s, $c, $params ) = @_; + my ( $s, $c ) = @_; + + my $params = $c->cobrand->disambiguate_location($s); + + $s = FixMyStreet::Geocode::escape($s); $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; + my $url = "${nominatimbase}search?"; my %query_params = ( q => $s, diff --git a/perllib/FixMyStreet/Geocode/Zurich.pm b/perllib/FixMyStreet/Geocode/Zurich.pm new file mode 100644 index 000000000..7ba3d27ad --- /dev/null +++ b/perllib/FixMyStreet/Geocode/Zurich.pm @@ -0,0 +1,114 @@ +#!/usr/bin/perl +# +# FixMyStreet::Geocode::Zurich +# Geocoding with Zurich web service. +# +# Thanks to http://msdn.microsoft.com/en-us/library/ms995764.aspx +# and http://noisemore.wordpress.com/2009/03/19/perl-soaplite-wsse-web-services-security-soapheader/ +# for SOAP::Lite pointers +# +# Copyright (c) 2012 UK Citizens Online Democracy. All rights reserved. +# Email: matthew@mysociety.org; WWW: http://www.mysociety.org/ + +package FixMyStreet::Geocode::Zurich; + +use strict; +use Digest::MD5 qw(md5_hex); +use File::Path (); +use Geo::Coordinates::CH1903; +use SOAP::Lite; +use Storable; +use mySociety::Locale; + +my ($soap, $method, $security); + +sub setup_soap { + return if $soap; + + # Variables for the SOAP web service + my $geocoder = FixMyStreet->config('GEOCODER'); + my $url = $geocoder->{url}; + my $username = $geocoder->{username}; + my $password = $geocoder->{password}; + my $attr = 'http://ch/geoz/fixmyzuerich/service'; + my $action = "$attr/IFixMyZuerich/"; + + # Set up the SOAP handler + $security = SOAP::Header->name("Security")->attr({ + 'mustUnderstand' => 'true', + 'xmlns' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' + })->value( + \SOAP::Header->name( + "UsernameToken" => \SOAP::Header->value( + SOAP::Header->name('Username', $username), + SOAP::Header->name('Password', $password) + ) + ) + ); + $soap = SOAP::Lite->on_action( sub { $action . $_[1]; } )->proxy($url); + $method = SOAP::Data->name('getLocation')->attr({ xmlns => $attr }); +} + +# string STRING CONTEXT +# Looks up on Zurich web service a user-inputted location. +# Returns array of (LAT, LON, ERROR), where ERROR is either undef, a string, or +# an array of matches if there are more than one. +# If there is no ambiguity, returns only a {lat,long} hash, unless allow_single_match_string is true +# (because the auto-complete use of this (in /around) should send the matched name even though it's not ambiguous). +# +# The information in the query may be used to disambiguate the location in cobranded +# versions of the site. + +sub string { + my ( $s, $c ) = @_; + + setup_soap(); + + my $cache_dir = FixMyStreet->config('GEO_CACHE') . 'zurich/'; + my $cache_file = $cache_dir . md5_hex($s); + my $result; + if (-s $cache_file) { + $result = retrieve($cache_file); + } else { + my $search = SOAP::Data->name('search' => $s)->type(''); + my $count = SOAP::Data->name('count' => 10)->type(''); + eval { + $result = $soap->call($method, $security, $search, $count); + }; + if ($@) { + warn $@ if FixMyStreet->config('STAGING_SITE'); + return { error => 'The geocoder appears to be down.' }; + } + $result = $result->result; + File::Path::mkpath($cache_dir); + store $result, $cache_file if $result; + } + + if (!$result || !$result->{Location}) { + return { error => _('Sorry, we could not parse that location. Please try again.') }; + } + + my $results = $result->{Location}; + $results = [ $results ] unless ref $results eq 'ARRAY'; + + my ( $error, @valid_locations, $latitude, $longitude ); + foreach (@$results) { + ($latitude, $longitude) = Geo::Coordinates::CH1903::to_latlon($_->{easting}, $_->{northing}); + mySociety::Locale::in_gb_locale { + push (@$error, { + address => $_->{text}, + latitude => sprintf('%0.6f', $latitude), + longitude => sprintf('%0.6f', $longitude) + }); + }; + push (@valid_locations, $_); + last if lc($_->{text}) eq lc($s); + } + if (scalar @valid_locations == 1 && ! $c->stash->{allow_single_geocode_match_strings} ) { + return { latitude => $latitude, longitude => $longitude }; + } + return { error => $error }; +} + +1; + diff --git a/perllib/FixMyStreet/Map.pm b/perllib/FixMyStreet/Map.pm index 587c63d25..f2dd0da6d 100644 --- a/perllib/FixMyStreet/Map.pm +++ b/perllib/FixMyStreet/Map.pm @@ -114,7 +114,26 @@ sub _map_features { } sub map_pins { - return $map_class->map_pins(@_); + my ($c, $interval) = @_; + + my $bbox = $c->req->param('bbox'); + my ( $min_lon, $min_lat, $max_lon, $max_lat ) = split /,/, $bbox; + + my ( $around_map, $around_map_list, $nearby, $dist ) = + FixMyStreet::Map::map_features_bounds( $c, $min_lon, $min_lat, $max_lon, $max_lat, $interval ); + + # 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 + ] + } @$around_map, @$nearby; + + return (\@pins, $around_map_list, $nearby, $dist); } sub click_to_wgs84 { diff --git a/perllib/FixMyStreet/Map/Google.pm b/perllib/FixMyStreet/Map/Google.pm index c0d83e35a..9deefc033 100644 --- a/perllib/FixMyStreet/Map/Google.pm +++ b/perllib/FixMyStreet/Map/Google.pm @@ -3,12 +3,17 @@ # FixMyStreet:Map::Google # Google maps on FixMyStreet. # -# Copyright (c) 2010 UK Citizens Online Democracy. All rights reserved. +# Copyright (c) 2013 UK Citizens Online Democracy. All rights reserved. # Email: matthew@mysociety.org; WWW: http://www.mysociety.org/ package FixMyStreet::Map::Google; use strict; +use mySociety::Gaze; +use Utils; + +use constant ZOOM_LEVELS => 6; +use constant MIN_ZOOM_LEVEL => 13; # display_map C PARAMS # PARAMS include: @@ -17,9 +22,37 @@ use strict; # PINS is array of pins to show, location and colour sub display_map { my ($self, $c, %params) = @_; + + my $numZoomLevels = ZOOM_LEVELS; + my $zoomOffset = MIN_ZOOM_LEVEL; + if ($params{any_zoom}) { + $numZoomLevels = 19; + $zoomOffset = 0; + } + + # Adjust zoom level dependent upon population density + my $dist = $c->stash->{distance} + || mySociety::Gaze::get_radius_containing_population( $params{latitude}, $params{longitude}, 200_000 ); + my $default_zoom = $c->cobrand->default_map_zoom() ? $c->cobrand->default_map_zoom() : $numZoomLevels - 4; + $default_zoom = $numZoomLevels - 3 if $dist < 10; + + # Map centre may be overridden in the query string + $params{latitude} = Utils::truncate_coordinate($c->req->params->{lat} + 0) + if defined $c->req->params->{lat}; + $params{longitude} = Utils::truncate_coordinate($c->req->params->{lon} + 0) + if defined $c->req->params->{lon}; + + my $zoom = defined $c->req->params->{zoom} ? $c->req->params->{zoom} + 0 : $default_zoom; + $zoom = $numZoomLevels - 1 if $zoom >= $numZoomLevels; + $zoom = 0 if $zoom < 0; + $params{zoom_act} = $zoomOffset + $zoom; + $c->stash->{map} = { %params, type => 'google', + zoom => $zoom, + zoomOffset => $zoomOffset, + numZoomLevels => $numZoomLevels, }; } diff --git a/perllib/FixMyStreet/Map/GoogleOL.pm b/perllib/FixMyStreet/Map/GoogleOL.pm new file mode 100644 index 000000000..64baf8d36 --- /dev/null +++ b/perllib/FixMyStreet/Map/GoogleOL.pm @@ -0,0 +1,22 @@ +#!/usr/bin/perl +# +# FixMyStreet:Map::GoogleOL +# Google maps on FixMyStreet, using OpenLayers. +# +# Copyright (c) 2013 UK Citizens Online Democracy. All rights reserved. +# Email: matthew@mysociety.org; WWW: http://www.mysociety.org/ + +package FixMyStreet::Map::GoogleOL; +use parent 'FixMyStreet::Map::OSM'; + +use strict; + +sub map_type { + return '""'; +} + +sub map_template { + return 'google-ol'; +} + +1; diff --git a/perllib/FixMyStreet/Map/OSM.pm b/perllib/FixMyStreet/Map/OSM.pm index 6b3bebba2..74af0e9f3 100644 --- a/perllib/FixMyStreet/Map/OSM.pm +++ b/perllib/FixMyStreet/Map/OSM.pm @@ -13,7 +13,7 @@ use Math::Trig; use mySociety::Gaze; use Utils; -use constant ZOOM_LEVELS => 5; +use constant ZOOM_LEVELS => 6; use constant MIN_ZOOM_LEVEL => 13; sub map_type { @@ -55,15 +55,15 @@ sub display_map { my $numZoomLevels = ZOOM_LEVELS; my $zoomOffset = MIN_ZOOM_LEVEL; if ($params{any_zoom}) { - $numZoomLevels = 18; + $numZoomLevels = 19; $zoomOffset = 0; } # Adjust zoom level dependent upon population density my $dist = $c->stash->{distance} || mySociety::Gaze::get_radius_containing_population( $params{latitude}, $params{longitude}, 200_000 ); - my $default_zoom = $c->cobrand->default_map_zoom() ? $c->cobrand->default_map_zoom() : $numZoomLevels - 3; - $default_zoom = $numZoomLevels - 2 if $dist < 10; + my $default_zoom = $c->cobrand->default_map_zoom() ? $c->cobrand->default_map_zoom() : $numZoomLevels - 4; + $default_zoom = $numZoomLevels - 3 if $dist < 10; # Map centre may be overridden in the query string $params{latitude} = Utils::truncate_coordinate($c->req->params->{lat} + 0) @@ -94,29 +94,6 @@ sub display_map { }; } -sub map_pins { - my ($self, $c, $interval) = @_; - - my $bbox = $c->req->param('bbox'); - my ( $min_lon, $min_lat, $max_lon, $max_lat ) = split /,/, $bbox; - - my ( $around_map, $around_map_list, $nearby, $dist ) = - FixMyStreet::Map::map_features_bounds( $c, $min_lon, $min_lat, $max_lon, $max_lat, $interval ); - - # 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 - ] - } @$around_map, @$nearby; - - return (\@pins, $around_map_list, $nearby, $dist); -} - sub compass { my ( $x, $y, $z ) = @_; return { diff --git a/perllib/FixMyStreet/Map/OSM/MapQuest.pm b/perllib/FixMyStreet/Map/OSM/MapQuest.pm index 4751679f5..a7f1b334e 100644 --- a/perllib/FixMyStreet/Map/OSM/MapQuest.pm +++ b/perllib/FixMyStreet/Map/OSM/MapQuest.pm @@ -15,6 +15,10 @@ sub map_type { return 'OpenLayers.Layer.OSM.MapQuestOpen'; } +sub map_template { + return 'mapquest-attribution'; +} + sub map_tiles { my ( $self, %params ) = @_; my ( $x, $y, $z ) = ( $params{x_tile}, $params{y_tile}, $params{zoom_act} ); @@ -28,7 +32,7 @@ sub map_tiles { } sub base_tile_url { - return 'mqcdn.com/tiles/1.0.0/osm/'; + return 'mqcdn.com/tiles/1.0.0/map/'; } 1; diff --git a/perllib/FixMyStreet/Map/Zurich.pm b/perllib/FixMyStreet/Map/Zurich.pm new file mode 100644 index 000000000..e09f8c90f --- /dev/null +++ b/perllib/FixMyStreet/Map/Zurich.pm @@ -0,0 +1,169 @@ +#!/usr/bin/perl +# +# FixMyStreet:Map::Zurich +# Zurich have their own tileserver. +# +# Copyright (c) 2012 UK Citizens Online Democracy. All rights reserved. +# Email: steve@mysociety.org; WWW: http://www.mysociety.org/ + +package FixMyStreet::Map::Zurich; + +use strict; +use Geo::Coordinates::CH1903; +use Math::Trig; +use Utils; + +use constant ZOOM_LEVELS => 8; +use constant DEFAULT_ZOOM => 5; +use constant MIN_ZOOM_LEVEL => 0; +use constant ID_OFFSET => 2; + +sub map_tiles { + my ( $self, %params ) = @_; + my ( $col, $row, $z ) = ( $params{x_tile}, $params{y_tile}, $params{matrix_id} ); + my $tile_url = $self->base_tile_url(); + return [ + "$tile_url/$z/" . ($row - 1) . "/" . ($col - 1) . ".jpg", + "$tile_url/$z/" . ($row - 1) . "/$col.jpg", + "$tile_url/$z/$row/" . ($col - 1) . ".jpg", + "$tile_url/$z/$row/$col.jpg", + ]; +} + +sub base_tile_url { + return '/maps/Hybrid/1.0.0/Hybrid/default/nativeTileMatrixSet'; +} + +sub copyright { + return '© Stadt Zürich'; +} + +# display_map C PARAMS +# PARAMS include: +# latitude, longitude for the centre point of the map +# CLICKABLE is set if the map is clickable +# PINS is array of pins to show, location and colour +sub display_map { + my ($self, $c, %params) = @_; + + # Map centre may be overridden in the query string + $params{latitude} = Utils::truncate_coordinate($c->req->params->{lat} + 0) + if defined $c->req->params->{lat}; + $params{longitude} = Utils::truncate_coordinate($c->req->params->{lon} + 0) + if defined $c->req->params->{lon}; + + my $zoom = defined $c->req->params->{zoom} + ? $c->req->params->{zoom} + 0 + : $c->stash->{page} eq 'report' + ? DEFAULT_ZOOM+1 + : DEFAULT_ZOOM; + $zoom = ZOOM_LEVELS - 1 if $zoom >= ZOOM_LEVELS; + $zoom = 0 if $zoom < 0; + + ($params{x_tile}, $params{y_tile}, $params{matrix_id}) = latlon_to_tile_with_adjust($params{latitude}, $params{longitude}, $zoom); + + foreach my $pin (@{$params{pins}}) { + ($pin->{px}, $pin->{py}) = latlon_to_px($pin->{latitude}, $pin->{longitude}, $params{x_tile}, $params{y_tile}, $zoom); + } + + $c->stash->{map} = { + %params, + type => 'zurich', + map_type => 'OpenLayers.Layer.WMTS', + tiles => $self->map_tiles( %params ), + copyright => $self->copyright(), + zoom => $zoom, + zoomOffset => MIN_ZOOM_LEVEL, + numZoomLevels => ZOOM_LEVELS, + }; +} + +# Given a lat/lon, convert it to Zurch tile co-ordinates (precise). +sub latlon_to_tile($$$) { + my ($lat, $lon, $zoom) = @_; + + my ($x, $y) = Geo::Coordinates::CH1903::from_latlon($lat, $lon); + + my $matrix_id = $zoom + ID_OFFSET; + my @scales = ( '250000', '125000', '64000', '32000', '16000', '8000', '4000', '2000', '1000', '500' ); + my $tileOrigin = { lat => 30814423, lon => -29386322 }; + my $tileSize = 256; + my $res = $scales[$matrix_id] / (39.3701 * 96); # OpenLayers.INCHES_PER_UNIT[units] * OpenLayers.DOTS_PER_INCH + + my $fx = ( $x - $tileOrigin->{lon} ) / ($res * $tileSize); + my $fy = ( $tileOrigin->{lat} - $y ) / ($res * $tileSize); + + return ( $fx, $fy, $matrix_id ); +} + +# Given a lat/lon, convert it to OSM tile co-ordinates (nearest actual tile, +# adjusted so the point will be near the centre of a 2x2 tiled map). +sub latlon_to_tile_with_adjust($$$) { + my ($lat, $lon, $zoom) = @_; + my ($x_tile, $y_tile, $matrix_id) = latlon_to_tile($lat, $lon, $zoom); + + # Try and have point near centre of map + if ($x_tile - int($x_tile) > 0.5) { + $x_tile += 1; + } + if ($y_tile - int($y_tile) > 0.5) { + $y_tile += 1; + } + + return ( int($x_tile), int($y_tile), $matrix_id ); +} + +sub tile_to_latlon { + my ($fx, $fy, $zoom) = @_; + + my $matrix_id = $zoom + ID_OFFSET; + my @scales = ( '250000', '125000', '64000', '32000', '16000', '8000', '4000', '2000', '1000', '500' ); + my $tileOrigin = { lat => 30814423, lon => -29386322 }; + my $tileSize = 256; + my $res = $scales[$matrix_id] / (39.3701 * 96); # OpenLayers.INCHES_PER_UNIT[units] * OpenLayers.DOTS_PER_INCH + + my $x = $fx * $res * $tileSize + $tileOrigin->{lon}; + my $y = $tileOrigin->{lat} - $fy * $res * $tileSize; + + my ($lat, $lon) = Geo::Coordinates::CH1903::to_latlon($x, $y); + + return ( $lat, $lon ); +} + +# Given a lat/lon, convert it to pixel co-ordinates from the top left of the map +sub latlon_to_px($$$$$) { + my ($lat, $lon, $x_tile, $y_tile, $zoom) = @_; + my ($pin_x_tile, $pin_y_tile) = latlon_to_tile($lat, $lon, $zoom); + my $pin_x = tile_to_px($pin_x_tile, $x_tile); + my $pin_y = tile_to_px($pin_y_tile, $y_tile); + return ($pin_x, $pin_y); +} + +# Convert tile co-ordinates to pixel co-ordinates from top left of map +# C is centre tile reference of displayed map +sub tile_to_px { + my ($p, $c) = @_; + $p = 256 * ($p - $c + 1); + $p = int($p + .5 * ($p <=> 0)); + return $p; +} + +sub click_to_tile { + my ($pin_tile, $pin) = @_; + $pin -= 256 while $pin > 256; + $pin += 256 while $pin < 0; + return $pin_tile + $pin / 256; +} + +# Given some click co-ords (the tile they were on, and where in the +# tile they were), convert to WGS84 and return. +sub click_to_wgs84 { + my ($self, $c, $pin_tile_x, $pin_x, $pin_tile_y, $pin_y) = @_; + my $tile_x = click_to_tile($pin_tile_x, $pin_x); + my $tile_y = click_to_tile($pin_tile_y, $pin_y); + my $zoom = (defined $c->req->params->{zoom} ? $c->req->params->{zoom} : DEFAULT_ZOOM); + my ($lat, $lon) = tile_to_latlon($tile_x, $tile_y, $zoom); + return ( $lat, $lon ); +} + +1; diff --git a/perllib/FixMyStreet/SendReport.pm b/perllib/FixMyStreet/SendReport.pm index 9ba507862..5087c7ead 100644 --- a/perllib/FixMyStreet/SendReport.pm +++ b/perllib/FixMyStreet/SendReport.pm @@ -7,17 +7,27 @@ use Module::Pluggable search_path => __PACKAGE__, require => 1; -has 'councils' => ( is => 'rw', isa => 'HashRef', default => sub { {} } ); +has 'body_config' => ( is => 'rw', isa => 'HashRef', default => sub { {} } ); +has 'bodies' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } ); has 'to' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } ); has 'success' => ( is => 'rw', isa => 'Bool', default => 0 ); has 'error' => ( is => 'rw', isa => 'Str', default => '' ); -has 'skipped' => ( 'is' => 'rw', isa => 'Str', default => '' ); has 'unconfirmed_counts' => ( 'is' => 'rw', isa => 'HashRef', default => sub { {} } ); has 'unconfirmed_notes' => ( 'is' => 'rw', isa => 'HashRef', default => sub { {} } ); sub should_skip { - return 0; + my $self = shift; + my $row = shift; + + return 0 unless $row->send_fail_count; + + my $tz = DateTime::TimeZone->new( name => 'local' ); + my $now = DateTime->now( time_zone => $tz ); + my $diff = $now - $row->send_fail_timestamp; + + my $backoff = $row->send_fail_count > 1 ? 30 : 5; + return $diff->in_units( 'minutes' ) < $backoff; } sub get_senders { @@ -31,18 +41,18 @@ sub get_senders { sub reset { my $self = shift; - $self->councils( {} ); + $self->bodies( [] ); + $self->body_config( {} ); $self->to( [] ); } -sub add_council { +sub add_body { my $self = shift; - my $council = shift; - my $info = shift; + my $body = shift; my $config = shift; - $self->councils->{ $council } = { info => $info, config => $config }; + push @{$self->bodies}, $body; + $self->body_config->{ $body->id } = $config; } - 1; diff --git a/perllib/FixMyStreet/SendReport/Barnet.pm b/perllib/FixMyStreet/SendReport/Barnet.pm index 9a92686ec..07adb4c33 100644 --- a/perllib/FixMyStreet/SendReport/Barnet.pm +++ b/perllib/FixMyStreet/SendReport/Barnet.pm @@ -4,37 +4,15 @@ use Moose; BEGIN { extends 'FixMyStreet::SendReport'; } -use BarnetInterfaces::service::ZLBB_SERVICE_ORDER; use Encode; use Utils; use mySociety::Config; use mySociety::Web qw(ent); -# maximum number of webservice attempts to send before not trying any more (XXX may be better in config?) -use constant SEND_FAIL_RETRIES_CUTOFF => 3; - # specific council numbers use constant COUNCIL_ID_BARNET => 2489; use constant MAX_LINE_LENGTH => 132; -sub should_skip { - my $self = shift; - my $row = shift; - - my $council_name = 'Barnet'; - my $err_msg = ""; - - if ($row->send_fail_count >= SEND_FAIL_RETRIES_CUTOFF) { - $council_name &&= " to $council_name"; - $err_msg = "skipped: problem id=" . $row->id . " send$council_name has failed " - . $row->send_fail_count . " times, cutoff is " . SEND_FAIL_RETRIES_CUTOFF; - - $self->skipped( $err_msg ); - - return 1; - } -} - sub construct_message { my %h = @_; my $message = <<EOF; @@ -66,6 +44,7 @@ sub send { my $geo_code = "$h{easting} $h{northing}"; + require BarnetInterfaces::service::ZLBB_SERVICE_ORDER; my $interface = BarnetInterfaces::service::ZLBB_SERVICE_ORDER->new(); my ($nearest_postcode, $nearest_street) = ('', ''); @@ -80,9 +59,9 @@ sub send { ? $h{query} : $nearest_postcode; # use given postcode if available # note: endpoint can be of form 'https://username:password@url' - my $council_config = FixMyStreet::App->model("DB::Open311conf")->search( { area_id => COUNCIL_ID_BARNET} )->first; - if ($council_config and $council_config->endpoint) { - $interface->set_proxy($council_config->endpoint); + my $body = FixMyStreet::App->model("DB::Body")->search( { 'body_areas.area_id' => COUNCIL_ID_BARNET }, { join => "body_areas" } )->first; + if ($body and $body->endpoint) { + $interface->set_proxy($body->endpoint); # Barnet web service doesn't like namespaces in the elements so use a prefix $interface->set_prefix('urn'); # uncomment these lines to print XML that will be sent rather @@ -90,7 +69,7 @@ sub send { #$interface->outputxml(1); #$interface->no_dispatch(1); } else { - die "Barnet webservice FAIL: looks like you're missing some config data: no endpoint (URL) found for area_id=" . COUNCIL_ID_BARNET; + die "Barnet webservice FAIL: looks like you're missing some config data: no endpoint (URL) found for area ID " . COUNCIL_ID_BARNET; } eval { diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm index b69436dd7..d71700020 100644 --- a/perllib/FixMyStreet/SendReport/Email.pm +++ b/perllib/FixMyStreet/SendReport/Email.pm @@ -11,30 +11,46 @@ sub build_recipient_list { my %recips; my $all_confirmed = 1; - foreach my $council ( keys %{ $self->councils } ) { + foreach my $body ( @{ $self->bodies } ) { my $contact = FixMyStreet::App->model("DB::Contact")->find( { deleted => 0, - area_id => $council, + body_id => $body->id, category => $row->category } ); - my ($council_email, $confirmed, $note) = ( $contact->email, $contact->confirmed, $contact->note ); + my ($body_email, $confirmed, $note) = ( $contact->email, $contact->confirmed, $contact->note ); - $council_email = essex_contact($row->latitude, $row->longitude) if $council == 2225; - $council_email = oxfordshire_contact($row->latitude, $row->longitude) if $council == 2237 && $council_email eq 'SPECIAL'; + $body_email = essex_contact($row->latitude, $row->longitude) if $body->areas->{2225}; + $body_email = oxfordshire_contact($row->latitude, $row->longitude) if $body->areas->{2237} && $body_email eq 'SPECIAL'; unless ($confirmed) { $all_confirmed = 0; - $note = 'Council ' . $row->council . ' deleted' + $note = 'Body ' . $row->bodies_str . ' deleted' unless $note; - $council_email = 'N/A' unless $council_email; - $self->unconfirmed_counts->{$council_email}{$row->category}++; - $self->unconfirmed_notes->{$council_email}{$row->category} = $note; + $body_email = 'N/A' unless $body_email; + $self->unconfirmed_counts->{$body_email}{$row->category}++; + $self->unconfirmed_notes->{$body_email}{$row->category} = $note; } - push @{ $self->to }, [ $council_email, $self->councils->{ $council }->{info}->{name} ]; - $recips{$council_email} = 1; + my $body_name = $body->name; + # see something uses council areas but doesn't send to councils so just use a + # generic name here to minimise confusion + if ( $row->cobrand eq 'seesomething' ) { + $body_name = 'See Something, Say Something'; + } + + my @emails; + # allow multiple emails per contact + if ( $body_email =~ /,/ ) { + @emails = split(/,/, $body_email); + } else { + @emails = ( $body_email ); + } + for my $email ( @emails ) { + push @{ $self->to }, [ $email, $body_name ]; + $recips{$email} = 1; + } } return () unless $all_confirmed; @@ -45,7 +61,7 @@ sub get_template { my ( $self, $row ) = @_; my $template = 'submit.txt'; - $template = 'submit-brent.txt' if $row->council eq 2488 || $row->council eq 2237; + $template = 'submit-brent.txt' if $row->bodies_str eq 2488 || $row->bodies_str eq 2237; my $template_path = FixMyStreet->path_to( "templates", "email", $row->cobrand, $row->lang, $template )->stringify; $template_path = FixMyStreet->path_to( "templates", "email", $row->cobrand, $template )->stringify unless -e $template_path; @@ -55,15 +71,20 @@ sub get_template { return $template; } +sub send_from { + my ( $self, $row ) = @_; + return [ $row->user->email, $row->name ]; +} + sub send { my $self = shift; my ( $row, $h ) = @_; my @recips = $self->build_recipient_list( $row, $h ); - # on a staging server send emails to ourselves rather than the councils - if (mySociety::Config::get('STAGING_SITE') && !FixMyStreet->test_mode) { - @recips = ( mySociety::Config::get('CONTACT_EMAIL') ); + # on a staging server send emails to ourselves rather than the bodies + if (mySociety::Config::get('STAGING_SITE') && !mySociety::Config::get('SEND_REPORTS_ON_STAGING') && !FixMyStreet->test_mode) { + @recips = ( $row->user->email ); } unless ( @recips ) { @@ -77,7 +98,7 @@ sub send { _template_ => $self->get_template( $row ), _parameters_ => $h, To => $self->to, - From => [ $row->user->email, $row->name ], + From => $self->send_from( $row ), }, mySociety::Config::get('CONTACT_EMAIL'), \@recips, diff --git a/perllib/FixMyStreet/SendReport/EmptyHomes.pm b/perllib/FixMyStreet/SendReport/EmptyHomes.pm index 4a6f058fe..4bae6af46 100644 --- a/perllib/FixMyStreet/SendReport/EmptyHomes.pm +++ b/perllib/FixMyStreet/SendReport/EmptyHomes.pm @@ -3,6 +3,8 @@ package FixMyStreet::SendReport::EmptyHomes; use Moose; use namespace::autoclean; +use mySociety::MaPit; + BEGIN { extends 'FixMyStreet::SendReport::Email'; } sub build_recipient_list { @@ -10,35 +12,41 @@ sub build_recipient_list { my %recips; my $all_confirmed = 1; - foreach my $council ( keys %{ $self->councils } ) { + foreach my $body ( @{ $self->bodies } ) { my $contact = FixMyStreet::App->model("DB::Contact")->find( { deleted => 0, - area_id => $council, + body_id => $body->id, category => 'Empty property', } ); - my ($council_email, $confirmed, $note) = ( $contact->email, $contact->confirmed, $contact->note ); + my ($body_email, $confirmed, $note) = ( $contact->email, $contact->confirmed, $contact->note ); unless ($confirmed) { $all_confirmed = 0; - #$note = 'Council ' . $row->council . ' deleted' + #$note = 'Council ' . $row->body . ' deleted' #unless $note; - $council_email = 'N/A' unless $council_email; - #$notgot{$council_email}{$row->category}++; - #$note{$council_email}{$row->category} = $note; + $body_email = 'N/A' unless $body_email; + #$notgot{$body_email}{$row->category}++; + #$note{$body_email}{$row->category} = $note; } - push @{ $self->to }, [ $council_email, $self->councils->{ $council }->{ info }->{name} ]; - $recips{$council_email} = 1; + push @{ $self->to }, [ $body_email, $body->name ]; + $recips{$body_email} = 1; - my $country = $self->councils->{$council}->{country}; + my $area_info = mySociety::MaPit::call('area', $body->body_areas->first->area_id); + my $country = $area_info->{country}; if ($country eq 'W') { - $recips{ 'shelter@' . mySociety::Config::get('EMAIL_DOMAIN') } = 1; + $recips{ 'wales@' . mySociety::Config::get('EMAIL_DOMAIN') } = 1; + } elsif ($country eq 'S') { + $recips{ 'scotland@' . mySociety::Config::get('EMAIL_DOMAIN') } = 1; } else { $recips{ 'eha@' . mySociety::Config::get('EMAIL_DOMAIN') } = 1; } } + # Set address email parameter from added data + $h->{address} = $row->extra->{address}; + return () unless $all_confirmed; return keys %recips; } diff --git a/perllib/FixMyStreet/SendReport/London.pm b/perllib/FixMyStreet/SendReport/London.pm index 6e7951922..2c48a091c 100644 --- a/perllib/FixMyStreet/SendReport/London.pm +++ b/perllib/FixMyStreet/SendReport/London.pm @@ -79,7 +79,6 @@ sub send { my $response = $browser->post( mySociety::Config::get('LONDON_REPORTIT_URL'), \%params ); my $out = $response->content; if ($response->code ne 200) { - print "Failed to post $h->{id} to London API, response was " . $response->code . " $out\n"; $self->error( "Failed to post $h->{id} to London API, response was " . $response->code . " $out" ); return 1; } diff --git a/perllib/FixMyStreet/SendReport/NI.pm b/perllib/FixMyStreet/SendReport/NI.pm index 810ee60e2..e0ea24f9c 100644 --- a/perllib/FixMyStreet/SendReport/NI.pm +++ b/perllib/FixMyStreet/SendReport/NI.pm @@ -9,10 +9,10 @@ sub build_recipient_list { my %recips; my $all_confirmed = 1; - foreach my $council ( keys %{ $self->councils } ) { + foreach my $body ( @{ $self->bodies } ) { my $contact = FixMyStreet::App->model("DB::Contact")->find( { deleted => 0, - area_id => $council, + body_id => $body->id, category => $row->category } ); @@ -23,10 +23,10 @@ sub build_recipient_list { $email = 'N/A' unless $email; } - my $name = $self->councils->{$council}->{info}->{name}; + my $name = $body->name; if ( $email =~ /^roads.([^@]*)\@drdni/ ) { $name = "Roads Service (\u$1)"; - $h->{councils_name} = $name; + $h->{bodies_name} = $name; $row->external_body( 'Roads Service' ); } push @{ $self->to }, [ $email, $name ]; diff --git a/perllib/FixMyStreet/SendReport/Noop.pm b/perllib/FixMyStreet/SendReport/Noop.pm new file mode 100644 index 000000000..f2e0a3bdb --- /dev/null +++ b/perllib/FixMyStreet/SendReport/Noop.pm @@ -0,0 +1,12 @@ +package FixMyStreet::SendReport::Noop; + +use Moose; + +BEGIN { extends 'FixMyStreet::SendReport'; } + +# Always skip when using this method +sub should_skip { + return 1; +} + +1; diff --git a/perllib/FixMyStreet/SendReport/Open311.pm b/perllib/FixMyStreet/SendReport/Open311.pm index 8d7a418af..ce5ee80a0 100644 --- a/perllib/FixMyStreet/SendReport/Open311.pm +++ b/perllib/FixMyStreet/SendReport/Open311.pm @@ -9,17 +9,9 @@ use FixMyStreet::App; use mySociety::Config; use DateTime::Format::W3CDTF; use Open311; +use Readonly; -sub should_skip { - my $self = shift; - my $row = shift; - - if ( $row->send_fail_count > 0 ) { - if ( bromley_retry_timeout($row) ) { - return 1; - } - } -} +Readonly::Scalar my $COUNCIL_ID_OXFORDSHIRE => 2237; sub send { my $self = shift; @@ -27,17 +19,22 @@ sub send { my $result = -1; - foreach my $council ( keys %{ $self->councils } ) { - my $conf = $self->councils->{$council}->{config}; + foreach my $body ( @{ $self->bodies } ) { + my $conf = $self->body_config->{ $body->id }; my $always_send_latlong = 1; my $send_notpinpointed = 0; my $use_service_as_deviceid = 0; - my $basic_desc = 0; + my $extended_desc = 1; + + # To rollback temporary changes made by this function + my $revert = 0; # Extra bromley fields - if ( $row->council =~ /2482/ ) { + if ( $row->bodies_str eq '2482' ) { + + $revert = 1; my $extra = $row->extra; if ( $row->used_map || ( !$row->used_map && !$row->postcode ) ) { @@ -49,7 +46,7 @@ sub send { push @$extra, { name => 'report_title', value => $row->title }; push @$extra, { name => 'public_anonymity_required', value => $row->anonymous ? 'TRUE' : 'FALSE' }; push @$extra, { name => 'email_alerts_requested', value => 'FALSE' }; # always false as can never request them - push @$extra, { name => 'requested_datetime', value => DateTime::Format::W3CDTF->format_datetime($row->confirmed_local->set_nanosecond(0)) }; + push @$extra, { name => 'requested_datetime', value => DateTime::Format::W3CDTF->format_datetime($row->confirmed->set_nanosecond(0)) }; push @$extra, { name => 'email', value => $row->user->email }; $row->extra( $extra ); @@ -60,66 +57,92 @@ sub send { # make sure we have last_name attribute present in row's extra, so # it is passed correctly to Bromley as attribute[] if ( $row->cobrand ne 'bromley' ) { - my ( $firstname, $lastname ) = ( $row->user->name =~ /(\w+)\.?\s+(.+)/ ); + my ( $firstname, $lastname ) = ( $row->name =~ /(\w+)\.?\s+(.+)/ ); push @$extra, { name => 'last_name', value => $lastname }; } - $basic_desc = 1; + $extended_desc = 0; + } + + # extra Oxfordshire fields: send nearest street, postcode, northing and easting, and the FMS id + if ( $row->bodies_str =~ /$COUNCIL_ID_OXFORDSHIRE/ ) { + + my $extra = $row->extra; + push @$extra, { name => 'external_id', value => $row->id }; + push @$extra, { name => 'closest_address', value => $h->{closest_address} } if $h->{closest_address}; + if ( $row->used_map || ( !$row->used_map && !$row->postcode ) ) { + push @$extra, { name => 'northing', value => $h->{northing} }; + push @$extra, { name => 'easting', value => $h->{easting} }; + } + $row->extra( $extra ); + + $extended_desc = 'oxfordshire'; } # FIXME: we've already looked this up before my $contact = FixMyStreet::App->model("DB::Contact")->find( { deleted => 0, - area_id => $conf->area_id, + body_id => $body->id, category => $row->category } ); - my $open311 = Open311->new( + my %open311_params = ( jurisdiction => $conf->jurisdiction, endpoint => $conf->endpoint, api_key => $conf->api_key, always_send_latlong => $always_send_latlong, send_notpinpointed => $send_notpinpointed, use_service_as_deviceid => $use_service_as_deviceid, - basic_description => $basic_desc, + extended_description => $extended_desc, ); + if (FixMyStreet->test_mode) { + my $test_res = HTTP::Response->new(); + $test_res->code(200); + $test_res->message('OK'); + $test_res->content('<?xml version="1.0" encoding="utf-8"?><service_requests><request><service_request_id>248</service_request_id></request></service_requests>'); + $open311_params{test_mode} = 1; + $open311_params{test_get_returns} = { 'requests.xml' => $test_res }; + } + + my $open311 = Open311->new( %open311_params ); # non standard west berks end points - if ( $row->council =~ /2619/ ) { + if ( $row->bodies_str =~ /2619/ ) { $open311->endpoints( { services => 'Services', requests => 'Requests' } ); } + # non-standard Oxfordshire endpoint (because it's just a script, not a full Open311 service) + if ( $row->bodies_str =~ /$COUNCIL_ID_OXFORDSHIRE/ ) { + $open311->endpoints( { requests => 'open311_service_request.cgi' } ); + $revert = 1; + } + # required to get round issues with CRM constraints - if ( $row->council =~ /2218/ ) { + if ( $row->bodies_str =~ /2218/ ) { $row->user->name( $row->user->id . ' ' . $row->user->name ); + $revert = 1; } if ($row->cobrand eq 'fixmybarangay') { - # FixMyBarangay endpoints expect external_id as an attribute + # FixMyBarangay endpoints expect external_id as an attribute, as do Oxfordshire $row->extra( [ { 'name' => 'external_id', 'value' => $row->id } ] ); + $revert = 1; } my $resp = $open311->send_service_request( $row, $h, $contact->email ); # make sure we don't save user changes from above - if ( $row->council =~ /2218/ || $row->council =~ /2482/ || $row->cobrand eq 'fixmybarangay') { - $row->discard_changes(); - } + $row->discard_changes() if $revert; if ( $resp ) { $row->external_id( $resp ); $row->send_method_used('Open311'); - if ($row->cobrand eq 'fixmybarangay') { - # currently the only external body using Open311 is DPS - # (this will change when we have 'body' logic in place, meanwhile: hardcoded) - $row->external_body("DPS"); - } $result *= 0; $self->success( 1 ); } else { $result *= 1; # temporary fix to resolve some issues with west berks - if ( $row->council =~ /2619/ ) { + if ( $row->bodies_str =~ /2619/ ) { $result *= 0; } } @@ -130,17 +153,4 @@ sub send { return $result; } -sub bromley_retry_timeout { - my $row = shift; - - my $tz = DateTime::TimeZone->new( name => 'local' ); - my $now = DateTime->now( time_zone => $tz ); - my $diff = $now - $row->send_fail_timestamp; - if ( $diff->in_units( 'minutes' ) < 30 ) { - return 1; - } - - return 0; -} - 1; diff --git a/perllib/FixMyStreet/SendReport/Zurich.pm b/perllib/FixMyStreet/SendReport/Zurich.pm new file mode 100644 index 000000000..d46561e9e --- /dev/null +++ b/perllib/FixMyStreet/SendReport/Zurich.pm @@ -0,0 +1,73 @@ +package FixMyStreet::SendReport::Zurich; + +use Moose; + +BEGIN { extends 'FixMyStreet::SendReport::Email'; } + +sub build_recipient_list { + my ( $self, $row, $h ) = @_; + + # Only one body ever, most of the time with an email endpoint + my $body = @{ $self->bodies }[0]; + if ( $row->external_body ) { + $body = FixMyStreet::App->model("DB::Body")->find( { id => $row->external_body } ); + $h->{bodies_name} = $body->name; + } + my $body_email = $body->endpoint; + + my $parent = $body->parent; + if ($parent && !$parent->parent) { + # Division, might have an individual contact email address + my $contact = FixMyStreet::App->model("DB::Contact")->find( { + body_id => $body->id, + category => $row->category + } ); + $body_email = $contact->email if $contact && $contact->email; + } + + push @{ $self->to }, [ $body_email, $body->name ]; + return $body_email; +} + +sub get_template { + my ( $self, $row ) = @_; + + my $template; + if ( $row->state eq 'unconfirmed' || $row->state eq 'confirmed' ) { + $template = 'submit.txt'; + } elsif ( $row->state eq 'in progress' ) { + $template = 'submit-in-progress.txt'; + } elsif ( $row->state eq 'planned' ) { + $template = 'submit-feedback-pending.txt'; + } elsif ( $row->state eq 'closed' ) { + $template = 'submit-external.txt'; + if ( $row->extra->{third_personal} ) { + $template = 'submit-external-personal.txt'; + } + } + + my $template_path = FixMyStreet->path_to( "templates", "email", "zurich", $template )->stringify; + $template = Utils::read_file( $template_path ); + return $template; +} + +# Zurich emails come from the site itself, unless it's to an external body, +# in which case it's from the category/body +sub send_from { + my ( $self, $row ) = @_; + + if ( $row->external_body ) { + my $body = @{ $self->bodies }[0]; + my $body_email = $body->endpoint; + my $contact = FixMyStreet::App->model("DB::Contact")->find( { + body_id => $body->id, + category => $row->category + } ); + $body_email = $contact->email if $contact && $contact->email; + return [ $body_email, FixMyStreet->config('CONTACT_NAME') ]; + } + + return [ FixMyStreet->config('CONTACT_EMAIL'), FixMyStreet->config('CONTACT_NAME') ]; +} + +1; diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm index 7f81c0fc2..be8f004a5 100644 --- a/perllib/FixMyStreet/TestMech.pm +++ b/perllib/FixMyStreet/TestMech.pm @@ -87,8 +87,8 @@ sub log_in_ok { my $user = $mech->create_user_ok($email); - # store the old password and then change it - my $old_password = $user->password; + # remember the old password and then change it to a known one + my $old_password = $user->password || ''; $user->update( { password => 'secret' } ); # log in @@ -99,7 +99,19 @@ sub log_in_ok { $mech->logged_in_ok; # restore the password (if there was one) - $user->update( { password => $old_password } ) if $old_password; + if ($old_password) { + + # Use store_column and then make_column_dirty to bypass the filters that + # would hash the password, otherwise the password required ito log in + # would be the hash of the previous one. + $user->store_column("password", $old_password); + $user->make_column_dirty("password"); + $user->update(); + + # Belt and braces, check that the password has been correctly saved. + die "password not correctly restored after log_in_ok" + if $user->password ne $old_password; + } return $user; } @@ -243,7 +255,7 @@ sub import_errors { my @errors = # grep { $_ } # map { s{^ERROR:\s*(.*)$}{$1}g ? $_ : undef; } # - split m/\n+/, $mech->response->content; + split m/\n+/, $mech->content; return \@errors; } @@ -296,7 +308,7 @@ sub extract_location { $meta = $mech->extract_problem_meta; -Returns the problem meta information ( submitted by, at etc ) from a +Returns the problem meta information ( submitted by, at etc ) from a problem report page =cut @@ -519,11 +531,11 @@ sub get_ok_json { return decode_json( $res->content ); } -sub delete_problems_for_council { +sub delete_problems_for_body { my $mech = shift; - my $council = shift; + my $body = shift; - my $reports = FixMyStreet::App->model('DB::Problem')->search( { council => $council } ); + my $reports = FixMyStreet::App->model('DB::Problem')->search( { bodies_str => $body } ); if ( $reports ) { for my $r ( $reports->all ) { $r->comments->delete; @@ -532,8 +544,26 @@ sub delete_problems_for_council { } } -sub create_problems_for_council { - my ( $mech, $count, $council, $title, $params ) = @_; +sub create_body_ok { + my $self = shift; + my ( $id, $name ) = @_; + + my $params = { id => $id, name => $name }; + my $body = FixMyStreet::App->model('DB::Body')->find_or_create($params); + $body->update($params); # Make sure + ok $body, "found/created user for $id $name"; + + FixMyStreet::App->model('DB::BodyArea')->find_or_create({ + area_id => $id, + body_id => $id, + }); + + return $body; + +} + +sub create_problems_for_body { + my ( $mech, $count, $body, $title, $params ) = @_; my $dt = $params->{dt} || DateTime->now(); @@ -549,11 +579,11 @@ sub create_problems_for_council { while ($count) { my $default_params = { postcode => 'SW1A 1AA', - council => $council, + bodies_str => $body, areas => ',105255,11806,11828,2247,2504,', category => 'Other', - title => "$title Test $count for $council", - detail => "$title Test $count for $council Detail", + title => "$title Test $count for $body", + detail => "$title Test $count for $body Detail", used_map => 't', name => 'Test User', anonymous => 'f', diff --git a/perllib/Geo/Coordinates/CH1903.pm b/perllib/Geo/Coordinates/CH1903.pm new file mode 100644 index 000000000..612182152 --- /dev/null +++ b/perllib/Geo/Coordinates/CH1903.pm @@ -0,0 +1,105 @@ +#!/usr/bin/perl +# +# Geo::Coordinates::CH1903 +# Conversion between WGS84 and Swiss CH1903. +# +# Copyright (c) 2012 UK Citizens Online Democracy. This module is free +# software; you can redistribute it and/or modify it under the same terms as +# Perl itself. +# +# WWW: http://www.mysociety.org/ + +package Geo::Coordinates::CH1903; + +$Geo::Coordinates::CH1903::VERSION = '1.00'; + +use strict; + +=head1 NAME + +Geo::Coordinates::CH1903 + +=head1 VERSION + +1.00 + +=head1 SYNOPSIS + + use Geo::Coordinates::CH1903; + + my ($lat, $lon) = ...; + my ($e, $n) = Geo::Coordinates::CH1903::from_latlon($lat, $lon); + my ($lat, $lon) = Geo::Coordinates::CH1903::to_latlon($e, $n); + +=head1 FUNCTIONS + +=over 4 + +=cut + +sub from_latlon($$) { + my ($lat, $lon) = @_; + + $lat *= 3600; + $lon *= 3600; + + my $lat_aux = ($lat - 169028.66) / 10000; + my $lon_aux = ($lon - 26782.5) / 10000; + + my $x = 600072.37 + + (211455.93 * $lon_aux) + - (10938.51 * $lon_aux * $lat_aux) + - (0.36 * $lon_aux * $lat_aux**2) + - (44.54 * $lon_aux**3); + + my $y = 200147.07 + + (308807.95 * $lat_aux) + + (3745.25 * $lon_aux**2) + + (76.63 * $lat_aux**2) + - (194.56 * $lon_aux**2 * $lat_aux) + + (119.79 * $lat_aux**3); + + return ($x, $y); +} + +sub to_latlon($$) { + my ($x, $y) = @_; + + my $x_aux = ($x - 600000) / 1000000; + my $y_aux = ($y - 200000) / 1000000; + + my $lat = 16.9023892 + + (3.238272 * $y_aux) + - (0.270978 * $x_aux**2) + - (0.002528 * $y_aux**2) + - (0.0447 * $x_aux**2 * $y_aux) + - (0.0140 * $y_aux**3); + + my $lon = 2.6779094 + + (4.728982 * $x_aux) + + (0.791484 * $x_aux * $y_aux) + + (0.1306 * $x_aux * $y_aux**2) + - (0.0436 * $x_aux**3); + + $lat = $lat * 100 / 36; + $lon = $lon * 100 / 36; + + return ($lat, $lon); +} + +=head1 AUTHOR AND COPYRIGHT + +Maths courtesy of the Swiss Federal Office of Topography: +http://www.swisstopo.admin.ch/internet/swisstopo/en/home/products/software/products/skripts.html + +Written by Matthew Somerville + +Copyright (c) UK Citizens Online Democracy. + +This module is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + +=cut + +1; + diff --git a/perllib/Open311.pm b/perllib/Open311.pm index 52e17e99d..4f9b948ce 100644 --- a/perllib/Open311.pm +++ b/perllib/Open311.pm @@ -22,9 +22,10 @@ has success => ( is => 'rw', 'isa' => 'Bool', default => 0 ); has error => ( is => 'rw', 'isa' => 'Str', default => '' ); has always_send_latlong => ( is => 'ro', isa => 'Bool', default => 1 ); has send_notpinpointed => ( is => 'ro', isa => 'Bool', default => 0 ); -has basic_description => ( is => 'ro', isa => 'Bool', default => 0 ); +has extended_description => ( is => 'ro', isa => 'Str', default => 1 ); has use_service_as_deviceid => ( is => 'ro', isa => 'Bool', default => 0 ); has use_extended_updates => ( is => 'ro', isa => 'Bool', default => 0 ); +has extended_statuses => ( is => 'ro', isa => 'Bool', default => 0 ); before [ qw/get_service_list get_service_meta_info get_service_requests get_service_request_updates @@ -38,7 +39,11 @@ sub get_service_list { my $service_list_xml = $self->_get( $self->endpoints->{services} ); - return $self->_get_xml_object( $service_list_xml ); + if ( $service_list_xml ) { + return $self->_get_xml_object( $service_list_xml ); + } else { + return undef; + } } sub get_service_meta_info { @@ -95,15 +100,15 @@ sub _populate_service_request_params { my $service_code = shift; my $description; - if ( $self->basic_description ) { - $description = $problem->detail; - } else { + if ( $self->extended_description ) { $description = $self->_generate_service_request_description( $problem, $extra ); + } else { + $description = $problem->detail; } - my ( $firstname, $lastname ) = ( $problem->user->name =~ /(\w+)\.?\s+(.+)/ ); + my ( $firstname, $lastname ) = ( $problem->name =~ /(\w+)\.?\s+(.+)/ ); my $params = { email => $problem->user->email, @@ -167,16 +172,23 @@ sub _generate_service_request_description { my $problem = shift; my $extra = shift; - my $description = <<EOT; -title: @{[$problem->title()]} - -detail: @{[$problem->detail()]} - -url: $extra->{url} - -Submitted via FixMyStreet -EOT -; + my $description = ""; + if ($extra->{easting_northing}) { # Proxy for cobrand being in the UK + $description .= "detail: " . $problem->detail . "\n\n"; + $description .= "url: " . $extra->{url} . "\n\n"; + $description .= "Submitted via FixMyStreet\n"; + if ($self->extended_description ne 'oxfordshire') { + $description = "title: " . $problem->title . "\n\n$description"; + } + } elsif ($problem->cobrand eq 'fixamingata') { + $description .= "Beskrivning: " . $problem->detail . "\n\n"; + $description .= "Länk till ärendet: " . $extra->{url} . "\n\n"; + $description .= "Skickad via FixaMinGata\n"; + } else { + $description .= $problem->title . "\n\n"; + $description .= $problem->detail . "\n\n"; + $description .= $extra->{url} . "\n"; + } return $description; } @@ -282,11 +294,41 @@ sub _populate_service_request_update_params { my $name = $comment->name || $comment->user->name; my ( $firstname, $lastname ) = ( $name =~ /(\w+)\.?\s+(.+)/ ); + $lastname ||= '-'; + + # fall back to problem state as it's probably correct + my $state = $comment->problem_state || $comment->problem->state; + + my $status = 'OPEN'; + if ( $self->extended_statuses ) { + if ( FixMyStreet::DB::Result::Problem->fixed_states()->{$state} ) { + $status = 'FIXED'; + } elsif ( $state eq 'in progress' ) { + $status = 'IN_PROGRESS'; + } elsif ($state eq 'action scheduled' + || $state eq 'planned' ) { + $status = 'ACTION_SCHEDULED'; + } elsif ( $state eq 'investigating' ) { + $status = 'INVESTIGATING'; + } elsif ( $state eq 'duplicate' ) { + $status = 'DUPLICATE'; + } elsif ( $state eq 'not responsible' ) { + $status = 'NOT_COUNCILS_RESPONSIBILITY'; + } elsif ( $state eq 'unable to fix' ) { + $status = 'NO_FURTHER_ACTION'; + } elsif ( $state eq 'internal referral' ) { + $status = 'INTERNAL_REFERRAL'; + } + } else { + if ( !FixMyStreet::DB::Result::Problem->open_states()->{$state} ) { + $status = 'CLOSED'; + } + } my $params = { - updated_datetime => DateTime::Format::W3CDTF->format_datetime($comment->confirmed_local->set_nanosecond(0)), + updated_datetime => DateTime::Format::W3CDTF->format_datetime($comment->confirmed->set_nanosecond(0)), service_request_id => $comment->problem->external_id, - status => $comment->problem->is_open ? 'OPEN' : 'CLOSED', + status => $status, email => $comment->user->email, description => $comment->text, last_name => $lastname, diff --git a/perllib/Open311/GetServiceRequestUpdates.pm b/perllib/Open311/GetServiceRequestUpdates.pm index 0b4e037fd..f7b758137 100644 --- a/perllib/Open311/GetServiceRequestUpdates.pm +++ b/perllib/Open311/GetServiceRequestUpdates.pm @@ -5,17 +5,19 @@ use Open311; use FixMyStreet::App; use DateTime::Format::W3CDTF; -has council_list => ( is => 'ro' ); has system_user => ( is => 'rw' ); has start_date => ( is => 'ro', default => undef ); has end_date => ( is => 'ro', default => undef ); has suppress_alerts => ( is => 'rw', default => 0 ); has verbose => ( is => 'ro', default => 0 ); +Readonly::Scalar my $AREA_ID_BROMLEY => 2482; +Readonly::Scalar my $AREA_ID_OXFORDSHIRE => 2237; + sub fetch { my $self = shift; - my $councils = FixMyStreet::App->model('DB::Open311Conf')->search( + my $bodies = FixMyStreet::App->model('DB::Body')->search( { send_method => 'Open311', send_comments => 1, @@ -24,29 +26,34 @@ sub fetch { } ); - while ( my $council = $councils->next ) { + while ( my $body = $bodies->next ) { my $o = Open311->new( - endpoint => $council->endpoint, - api_key => $council->api_key, - jurisdiction => $council->jurisdiction, + endpoint => $body->endpoint, + api_key => $body->api_key, + jurisdiction => $body->jurisdiction, ); - if ( $council->area_id =~ /2482/ ) { + # custom endpoint URLs because these councils have non-standard paths + if ( $body->areas->{$AREA_ID_BROMLEY} ) { my $endpoints = $o->endpoints; $endpoints->{update} = 'update.xml'; $endpoints->{service_request_updates} = 'update.xml'; $o->endpoints( $endpoints ); + } elsif ( $body->areas->{$AREA_ID_OXFORDSHIRE} ) { + my $endpoints = $o->endpoints; + $endpoints->{service_request_updates} = 'open311_service_request_update.cgi'; + $o->endpoints( $endpoints ); } - $self->suppress_alerts( $council->suppress_alerts ); - $self->system_user( $council->comment_user ); - $self->update_comments( $o, { areaid => $council->area_id }, ); + $self->suppress_alerts( $body->suppress_alerts ); + $self->system_user( $body->comment_user ); + $self->update_comments( $o, { areas => $body->areas }, ); } } sub update_comments { - my ( $self, $open311, $council_details ) = @_; + my ( $self, $open311, $body_details ) = @_; my @args = (); @@ -56,7 +63,7 @@ sub update_comments { push @args, $self->start_date; push @args, $self->end_date; # default to asking for last 2 hours worth if not Bromley - } elsif ( $council_details->{areaid} != 2482 ) { + } elsif ( ! $body_details->{areas}->{$AREA_ID_BROMLEY} ) { my $end_dt = DateTime->now(); my $start_dt = $end_dt->clone; $start_dt->add( hours => -2 ); @@ -68,7 +75,7 @@ sub update_comments { my $requests = $open311->get_service_request_updates( @args ); unless ( $open311->success ) { - warn "Failed to fetch ServiceRequest Updates for " . $council_details->{areaid} . ":\n" . $open311->error + warn "Failed to fetch ServiceRequest Updates for " . join(",", keys %{$body_details->{areas}}) . ":\n" . $open311->error if $self->verbose; return 0; } @@ -80,12 +87,13 @@ sub update_comments { # what problem it belongs to so just skip next unless $request_id; - my $problem = - FixMyStreet::App->model('DB::Problem') - ->search( { - external_id => $request_id, - council => { like => '%' . $council_details->{areaid} . '%' }, - } ); + my $problem; + my $criteria = { + external_id => $request_id, + # XXX This assumes that areas will actually only be one area. + bodies_str => { like => '%' . join(",", keys %{$body_details->{areas}}) . '%' }, + }; + $problem = FixMyStreet::App->model('DB::Problem')->search( $criteria ); if (my $p = $problem->first) { my $c = $p->comments->search( { external_id => $request->{update_id} } ); @@ -112,13 +120,17 @@ sub update_comments { # if the comment is older than the last update # do not change the status of the problem as it's # tricky to determine the right thing to do. - if ( $comment->created_local > $p->lastupdate_local ) { - if ( $p->is_open and lc($request->{status}) eq 'closed' ) { - $p->state( 'fixed - council' ); - $comment->problem_state( 'fixed - council' ); - } elsif ( ( $p->is_closed || $p->is_fixed ) and lc($request->{status}) eq 'open' ) { - $p->state( 'confirmed' ); - $comment->problem_state( 'confirmed' ); + if ( $comment->created > $p->lastupdate ) { + my $state = $self->map_state( $request->{status} ); + + # 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 && + !( $p->is_fixed && FixMyStreet::DB::Result::Problem->fixed_states()->{$state} ) ) { + if ($p->is_visible) { + $p->state($state); + } + $comment->problem_state($state); } } @@ -127,17 +139,19 @@ sub update_comments { $comment->insert(); if ( $self->suppress_alerts ) { - my $alert = FixMyStreet::App->model('DB::Alert')->find( { + my @alerts = FixMyStreet::App->model('DB::Alert')->search( { alert_type => 'new_updates', parameter => $p->id, confirmed => 1, user_id => $p->user->id, } ); - my $alerts_sent = FixMyStreet::App->model('DB::AlertSent')->find_or_create( { - alert_id => $alert->id, - parameter => $comment->id, - } ); + for my $alert (@alerts) { + my $alerts_sent = FixMyStreet::App->model('DB::AlertSent')->find_or_create( { + alert_id => $alert->id, + parameter => $comment->id, + } ); + } } } } @@ -146,4 +160,22 @@ sub update_comments { return 1; } +sub map_state { + my $self = shift; + my $incoming_state = shift; + + $incoming_state = lc($incoming_state); + $incoming_state =~ s/_/ /g; + + my %state_map = ( + fixed => 'fixed - council', + 'not councils responsibility' => 'not responsible', + 'no further action' => 'unable to fix', + open => 'confirmed', + closed => 'fixed - council' + ); + + return $state_map{$incoming_state} || $incoming_state; +} + 1; diff --git a/perllib/Open311/GetUpdates.pm b/perllib/Open311/GetUpdates.pm index 5d5291d47..5007a1f82 100644 --- a/perllib/Open311/GetUpdates.pm +++ b/perllib/Open311/GetUpdates.pm @@ -4,26 +4,22 @@ use Moose; use Open311; use FixMyStreet::App; -has council_list => ( is => 'ro' ); +has body_list => ( is => 'ro' ); has system_user => ( is => 'ro' ); sub get_updates { my $self = shift; - while ( my $council = $self->council_list->next ) { + while ( my $body = $self->body_list->next ) { my $open311 = Open311->new( - endpoint => $council->endpoint, - jurisdiction => $council->jurisdiction, - api_key => $council->api_key + endpoint => $body->endpoint, + jurisdiction => $body->jurisdiction, + api_key => $body->api_key ); - my $area_id = $council->area_id; - - my $council_details = mySociety::MaPit::call( 'area', $area_id ); - my $reports = FixMyStreet::App->model('DB::Problem')->search( { - council => { like => "\%$area_id\%" }, + bodies_str => { like => "\%" . $body->id . "\%" }, state => { 'IN', [qw/confirmed fixed/] }, -and => [ external_id => { '!=', undef }, @@ -39,12 +35,12 @@ sub get_updates { next unless @report_ids; - $self->update_reports( \@report_ids, $open311, $council_details ); + $self->update_reports( \@report_ids, $open311, $body ); } } sub update_reports { - my ( $self, $report_ids, $open311, $council_details ) = @_; + my ( $self, $report_ids, $open311, $body ) = @_; my $service_requests = $open311->get_service_requests( $report_ids ); @@ -72,7 +68,9 @@ sub update_reports { ->search( { external_id => $request_id, } ); if (my $p = $problem->first) { - $p->update_from_open311_service_request( $request, $council_details, $self->system_user ); + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($p->cobrand)->new(); + $cobrand->set_lang_and_domain($p->lang, 1, FixMyStreet->path_to('locale')->stringify ); + $p->update_from_open311_service_request( $request, $body, $self->system_user ); } } diff --git a/perllib/Open311/PopulateServiceList.pm b/perllib/Open311/PopulateServiceList.pm index e8ae56833..c5fc4a506 100644 --- a/perllib/Open311/PopulateServiceList.pm +++ b/perllib/Open311/PopulateServiceList.pm @@ -6,34 +6,34 @@ use XML::Simple; use FixMyStreet::App; use Open311; -has council_list => ( is => 'ro' ); +has bodies => ( is => 'ro' ); has found_contacts => ( is => 'rw', default => sub { [] } ); has verbose => ( is => 'ro', default => 0 ); -has _current_council => ( is => 'rw' ); +has _current_body => ( is => 'rw' ); has _current_open311 => ( is => 'rw' ); has _current_service => ( is => 'rw' ); -my $council_list = FixMyStreet::App->model('DB::Open311conf'); +my $bodies = FixMyStreet::App->model('DB::Body'); -sub process_councils { +sub process_bodies { my $self = shift; - while ( my $council = $self->council_list->next ) { - next unless $council->endpoint; - next unless lc($council->send_method) eq 'open311'; - $self->_current_council( $council ); - $self->process_council; + while ( my $body = $self->bodies->next ) { + next unless $body->endpoint; + next unless lc($body->send_method) eq 'open311'; + next if $body->jurisdiction =~ /^fixmybarangay_\w+$/; # FMB depts. not using service discovery yet + $self->_current_body( $body ); + $self->process_body; } } -sub process_council { +sub process_body { my $self = shift; - my $open311 = Open311->new( - endpoint => $self->_current_council->endpoint, - jurisdiction => $self->_current_council->jurisdiction, - api_key => $self->_current_council->api_key + endpoint => $self->_current_body->endpoint, + jurisdiction => $self->_current_body->jurisdiction, + api_key => $self->_current_body->api_key ); $self->_current_open311( $open311 ); @@ -41,8 +41,10 @@ sub process_council { my $list = $open311->get_service_list; unless ( $list ) { - my $id = $self->_current_council->area_id; - warn "Council $id - http://mapit.mysociety.org/area/$id.html - did not return a service list\n" + my $id = $self->_current_body->id; + my $mapit_url = mySociety::Config::get('MAPIT_URL'); + my $areas = join( ",", keys %{$self->_current_body->areas} ); + warn "Body $id for areas $areas - $mapit_url/areas/$areas.html - did not return a service list\n" if $self->verbose >= 1; return; } @@ -55,7 +57,7 @@ sub _check_endpoints { my $self = shift; # west berks end point not standard - if ( $self->_current_council->area_id == 2619 ) { + if ( $self->_current_body->areas->{2619} ) { $self->_current_open311->endpoints( { services => 'Services', @@ -71,7 +73,10 @@ sub process_services { my $list = shift; $self->found_contacts( [] ); - foreach my $service ( @{ $list->{service} } ) { + my $services = $list->{service}; + # XML might only have one result and then squashed the 'array'-ness + $services = [ $services ] unless ref $services eq 'ARRAY'; + foreach my $service ( @$services ) { $self->_current_service( $service ); $self->process_service; } @@ -81,14 +86,14 @@ sub process_services { sub process_service { my $self = shift; - my $category = $self->_current_council->area_id == 2218 ? - $self->_current_service->{description} : + my $category = $self->_current_body->areas->{2218} ? + $self->_current_service->{description} : $self->_current_service->{service_name}; print $self->_current_service->{service_code} . ': ' . $category . "\n" if $self->verbose >= 2; my $contacts = FixMyStreet::App->model( 'DB::Contact')->search( { - area_id => $self->_current_council->area_id, + body_id => $self->_current_body->id, -OR => [ email => $self->_current_service->{service_code}, category => $category, @@ -125,7 +130,7 @@ sub _handle_existing_contact { my $service_name = $self->_normalize_service_name; - print $self->_current_council->area_id . " already has a contact for service code " . $self->_current_service->{service_code} . "\n" if $self->verbose >= 2; + 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 ) { eval { @@ -143,7 +148,7 @@ sub _handle_existing_contact { }; if ( $@ ) { - warn "Failed to update contact for service code " . $self->_current_service->{service_code} . " for council @{[$self->_current_council->area_id]}: $@\n" + warn "Failed to update contact for service code " . $self->_current_service->{service_code} . " for body @{[$self->_current_body->id]}: $@\n" if $self->verbose >= 1; return; } @@ -168,7 +173,7 @@ sub _create_contact { $contact = FixMyStreet::App->model( 'DB::Contact')->create( { email => $self->_current_service->{service_code}, - area_id => $self->_current_council->area_id, + body_id => $self->_current_body->id, category => $service_name, confirmed => 1, deleted => 0, @@ -180,7 +185,7 @@ sub _create_contact { }; if ( $@ ) { - warn "Failed to create contact for service code " . $self->_current_service->{service_code} . " for council @{[$self->_current_council->area_id]}: $@\n" + warn "Failed to create contact for service code " . $self->_current_service->{service_code} . " for body @{[$self->_current_body->id]}: $@\n" if $self->verbose >= 1; return; } @@ -191,7 +196,7 @@ sub _create_contact { if ( $contact ) { push @{ $self->found_contacts }, $self->_current_service->{service_code}; - print "created contact for service code " . $self->_current_service->{service_code} . " for council @{[$self->_current_council->area_id]}\n" if $self->verbose >= 2; + print "created contact for service code " . $self->_current_service->{service_code} . " for body @{[$self->_current_body->id]}\n" if $self->verbose >= 2; } } @@ -210,7 +215,7 @@ sub _add_meta_to_contact { if ( ! $meta_data->{attributes}->{attribute} ) { warn sprintf( "Empty meta data for %s at %s", $self->_current_service->{service_code}, - $self->_current_council->endpoint ) + $self->_current_body->endpoint ) if $self->verbose; return; } @@ -225,7 +230,7 @@ sub _add_meta_to_contact { # we add these later on from bromley so don't list them here # as we don't want to display them - if ( $self->_current_council->area_id == 2482 ) { + if ( $self->_current_body->areas->{2482} ) { my %ignore = map { $_ => 1 } qw/ service_request_id_ext requested_datetime @@ -256,8 +261,8 @@ sub _normalize_service_name { # FIXME - at the moment it makes more sense to use the description # for cambridgeshire but need a more flexible way to set this - my $service_name = $self->_current_council->area_id == 2218 ? - $self->_current_service->{description} : + my $service_name = $self->_current_body->areas->{2218} ? + $self->_current_service->{description} : $self->_current_service->{service_name}; # remove trailing whitespace as it upsets db queries # to look up contact details when creating problem @@ -272,7 +277,7 @@ sub _delete_contacts_not_in_service_list { my $found_contacts = FixMyStreet::App->model( 'DB::Contact')->search( { email => { -not_in => $self->found_contacts }, - area_id => $self->_current_council->area_id, + body_id => $self->_current_body->id, deleted => 0, } ); diff --git a/perllib/PoChange.pm b/perllib/PoChange.pm index 06b78fa3f..f26161889 100644 --- a/perllib/PoChange.pm +++ b/perllib/PoChange.pm @@ -41,7 +41,6 @@ sub fixmystreet_to_reportemptyhomes($) { $s =~ s/We send it to the council on your behalf/The details will be sent directly to the right person in the local council for them to take action/; $s =~ s/To find out what local alerts we have for you/To find out what local alerts we have in your area, council or ward/; $s =~ s/Local alerts/Get local reports/; - $s =~ s/All reports/Reports/; $s =~ s/Report an empty property/Report a property/; $s =~ s/Help/FAQs/; diff --git a/perllib/Template/Document.pm b/perllib/Template/Document.pm new file mode 100644 index 000000000..8fc66deea --- /dev/null +++ b/perllib/Template/Document.pm @@ -0,0 +1,539 @@ +##============================================================= -*-Perl-*- +# +# Template::Document +# +# DESCRIPTION +# Module defining a class of objects which encapsulate compiled +# templates, storing additional block definitions and metadata +# as well as the compiled Perl sub-routine representing the main +# template content. +# +# AUTHOR +# Andy Wardley <abw@wardley.org> +# +# COPYRIGHT +# Copyright (C) 1996-2007 Andy Wardley. All Rights Reserved. +# +# This module is free software; you can redistribute it and/or +# modify it under the same terms as Perl itself. +# +#============================================================================ + +package Template::Document; + +use strict; +use warnings; +use base 'Template::Base'; +use Template::Constants; + +our $VERSION = 2.79; +our $DEBUG = 0 unless defined $DEBUG; +our $ERROR = ''; +our ($COMPERR, $AUTOLOAD, $UNICODE); + +BEGIN { + # UNICODE is supported in versions of Perl from 5.008 onwards + if ($UNICODE = $] > 5.007 ? 1 : 0) { + if ($] > 5.008) { + # utf8::is_utf8() available from Perl 5.8.1 onwards + *is_utf8 = \&utf8::is_utf8; + } + elsif ($] == 5.008) { + # use Encode::is_utf8() for Perl 5.8.0 + require Encode; + *is_utf8 = \&Encode::is_utf8; + } + } +} + + +#======================================================================== +# ----- PUBLIC METHODS ----- +#======================================================================== + +#------------------------------------------------------------------------ +# new(\%document) +# +# Creates a new self-contained Template::Document object which +# encapsulates a compiled Perl sub-routine, $block, any additional +# BLOCKs defined within the document ($defblocks, also Perl sub-routines) +# and additional $metadata about the document. +#------------------------------------------------------------------------ + +sub new { + my ($class, $doc) = @_; + my ($block, $defblocks, $variables, $metadata) = @$doc{ qw( BLOCK DEFBLOCKS VARIABLES METADATA ) }; + $defblocks ||= { }; + $metadata ||= { }; + + # evaluate Perl code in $block to create sub-routine reference if necessary + unless (ref $block) { + local $SIG{__WARN__} = \&catch_warnings; + $COMPERR = ''; + + # DON'T LOOK NOW! - blindly untainting can make you go blind! + $block =~ /(.*)/s; + $block = $1; + + $block = eval $block; + return $class->error($@) + unless defined $block; + } + + # same for any additional BLOCK definitions + @$defblocks{ keys %$defblocks } = + # MORE BLIND UNTAINTING - turn away if you're squeamish + map { + ref($_) + ? $_ + : ( /(.*)/s && eval($1) or return $class->error($@) ) + } values %$defblocks; + + bless { + %$metadata, + _BLOCK => $block, + _DEFBLOCKS => $defblocks, + _VARIABLES => $variables, + _HOT => 0, + }, $class; +} + + +#------------------------------------------------------------------------ +# block() +# +# Returns a reference to the internal sub-routine reference, _BLOCK, +# that constitutes the main document template. +#------------------------------------------------------------------------ + +sub block { + return $_[0]->{ _BLOCK }; +} + + +#------------------------------------------------------------------------ +# blocks() +# +# Returns a reference to a hash array containing any BLOCK definitions +# from the template. The hash keys are the BLOCK nameand the values +# are references to Template::Document objects. Returns 0 (# an empty hash) +# if no blocks are defined. +#------------------------------------------------------------------------ + +sub blocks { + return $_[0]->{ _DEFBLOCKS }; +} + + +#----------------------------------------------------------------------- +# variables() +# +# Returns a reference to a hash of variables used in the template. +# This requires the TRACE_VARS option to be enabled. +#----------------------------------------------------------------------- + +sub variables { + return $_[0]->{ _VARIABLES }; +} + +#------------------------------------------------------------------------ +# process($context) +# +# Process the document in a particular context. Checks for recursion, +# registers the document with the context via visit(), processes itself, +# and then unwinds with a large gin and tonic. +#------------------------------------------------------------------------ + +sub process { + my ($self, $context) = @_; + my $defblocks = $self->{ _DEFBLOCKS }; + my $output; + + + # check we're not already visiting this template + return $context->throw(Template::Constants::ERROR_FILE, + "recursion into '$self->{ name }'") + if $self->{ _HOT } && ! $context->{ RECURSION }; ## RETURN ## + + $context->visit($self, $defblocks); + + $self->{ _HOT } = 1; + eval { + my $block = $self->{ _BLOCK }; + $output = &$block($context); + }; + $self->{ _HOT } = 0; + + $context->leave(); + + die $context->catch($@) + if $@; + + return $output; +} + + +#------------------------------------------------------------------------ +# AUTOLOAD +# +# Provides pseudo-methods for read-only access to various internal +# members. +#------------------------------------------------------------------------ + +sub AUTOLOAD { + my $self = shift; + my $method = $AUTOLOAD; + + $method =~ s/.*:://; + return if $method eq 'DESTROY'; +# my ($pkg, $file, $line) = caller(); +# print STDERR "called $self->AUTOLOAD($method) from $file line $line\n"; + return $self->{ $method }; +} + + +#======================================================================== +# ----- PRIVATE METHODS ----- +#======================================================================== + + +#------------------------------------------------------------------------ +# _dump() +# +# Debug method which returns a string representing the internal state +# of the object. +#------------------------------------------------------------------------ + +sub _dump { + my $self = shift; + my $dblks; + my $output = "$self : $self->{ name }\n"; + + $output .= "BLOCK: $self->{ _BLOCK }\nDEFBLOCKS:\n"; + + if ($dblks = $self->{ _DEFBLOCKS }) { + foreach my $b (keys %$dblks) { + $output .= " $b: $dblks->{ $b }\n"; + } + } + + return $output; +} + + +#======================================================================== +# ----- CLASS METHODS ----- +#======================================================================== + +#------------------------------------------------------------------------ +# as_perl($content) +# +# This method expects a reference to a hash passed as the first argument +# containing 3 items: +# METADATA # a hash of template metadata +# BLOCK # string containing Perl sub definition for main block +# DEFBLOCKS # hash containing further subs for addional BLOCK defs +# It returns a string containing Perl code which, when evaluated and +# executed, will instantiate a new Template::Document object with the +# above data. On error, it returns undef with an appropriate error +# message set in $ERROR. +#------------------------------------------------------------------------ + +sub as_perl { + my ($class, $content) = @_; + my ($block, $defblocks, $metadata) = @$content{ qw( BLOCK DEFBLOCKS METADATA ) }; + + #$block =~ s/\n(?!#line)/\n /g; + $block =~ s/\s+$//; + + $defblocks = join('', map { + my $code = $defblocks->{ $_ }; + # $code =~ s/\n(?!#line)/\n /g; + $code =~ s/\s*$//; + " '$_' => $code,\n"; + } keys %$defblocks); + $defblocks =~ s/\s+$//; + + $metadata = join('', map { + my $x = $metadata->{ $_ }; + $x =~ s/(['\\])/\\$1/g; + " '$_' => '$x',\n"; + } keys %$metadata); + $metadata =~ s/\s+$//; + + return <<EOF +#------------------------------------------------------------------------ +# Compiled template generated by the Template Toolkit version $Template::VERSION +#------------------------------------------------------------------------ + +$class->new({ + METADATA => { +$metadata + }, + BLOCK => $block, + DEFBLOCKS => { +$defblocks + }, +}); +EOF +} + + +#------------------------------------------------------------------------ +# write_perl_file($filename, \%content) +# +# This method calls as_perl() to generate the Perl code to represent a +# compiled template with the content passed as the second argument. +# It then writes this to the file denoted by the first argument. +# +# Returns 1 on success. On error, sets the $ERROR package variable +# to contain an error message and returns undef. +#------------------------------------------------------------------------ + +sub write_perl_file { + my ($class, $file, $content) = @_; + my ($fh, $tmpfile); + + return $class->error("invalid filename: $file") + unless $file =~ /^(.+)$/s; + + eval { + require File::Temp; + require File::Basename; + ($fh, $tmpfile) = File::Temp::tempfile( + DIR => File::Basename::dirname($file) + ); + my $perlcode = $class->as_perl($content) || die $!; + + if ($UNICODE && is_utf8($perlcode)) { + $perlcode = "use utf8;\n\n$perlcode"; + binmode $fh, ":utf8"; + } + print $fh $perlcode; + close($fh); + }; + return $class->error($@) if $@; + return rename($tmpfile, $file) + || $class->error($!); +} + + +#------------------------------------------------------------------------ +# catch_warnings($msg) +# +# Installed as +#------------------------------------------------------------------------ + +sub catch_warnings { + $COMPERR .= join('', @_); +} + + +1; + +__END__ + +=head1 NAME + +Template::Document - Compiled template document object + +=head1 SYNOPSIS + + use Template::Document; + + $doc = Template::Document->new({ + BLOCK => sub { # some perl code; return $some_text }, + DEFBLOCKS => { + header => sub { # more perl code; return $some_text }, + footer => sub { # blah blah blah; return $some_text }, + }, + METADATA => { + author => 'Andy Wardley', + version => 3.14, + } + }) || die $Template::Document::ERROR; + + print $doc->process($context); + +=head1 DESCRIPTION + +This module defines an object class whose instances represent compiled +template documents. The L<Template::Parser> module creates a +C<Template::Document> instance to encapsulate a template as it is compiled +into Perl code. + +The constructor method, L<new()>, expects a reference to a hash array +containing the C<BLOCK>, C<DEFBLOCKS> and C<METADATA> items. + +The C<BLOCK> item should contain a reference to a Perl subroutine or a textual +representation of Perl code, as generated by the L<Template::Parser> module. +This is then evaluated into a subroutine reference using C<eval()>. + +The C<DEFLOCKS> item should reference a hash array containing further named +C<BLOCK>s which may be defined in the template. The keys represent C<BLOCK> +names and the values should be subroutine references or text strings of Perl +code as per the main C<BLOCK> item. + +The C<METADATA> item should reference a hash array of metadata items relevant +to the document. + +The L<process()> method can then be called on the instantiated +C<Template::Document> object, passing a reference to a L<Template::Context> +object as the first parameter. This will install any locally defined blocks +(C<DEFBLOCKS>) in the C<BLOCKS> cache in the context (via a call to +L<visit()|Template::Context#visit()>) so that they may be subsequently +resolved by the context. The main C<BLOCK> subroutine is then executed, +passing the context reference on as a parameter. The text returned from the +template subroutine is then returned by the L<process()> method, after calling +the context L<leave()|Template::Context#leave()> method to permit cleanup and +de-registration of named C<BLOCKS> previously installed. + +An C<AUTOLOAD> method provides access to the C<METADATA> items for the +document. The L<Template::Service> module installs a reference to the main +C<Template::Document> object in the stash as the C<template> variable. This allows +metadata items to be accessed from within templates, including C<PRE_PROCESS> +templates. + +header: + + <html> + <head> + <title>[% template.title %] + </head> + ... + +C<Template::Document> objects are usually created by the L<Template::Parser> +but can be manually instantiated or sub-classed to provide custom +template components. + +=head1 METHODS + +=head2 new(\%config) + +Constructor method which accept a reference to a hash array containing the +structure as shown in this example: + + $doc = Template::Document->new({ + BLOCK => sub { # some perl code; return $some_text }, + DEFBLOCKS => { + header => sub { # more perl code; return $some_text }, + footer => sub { # blah blah blah; return $some_text }, + }, + METADATA => { + author => 'Andy Wardley', + version => 3.14, + } + }) || die $Template::Document::ERROR; + +C<BLOCK> and C<DEFBLOCKS> items may be expressed as references to Perl subroutines +or as text strings containing Perl subroutine definitions, as is generated +by the L<Template::Parser> module. These are evaluated into subroutine references +using C<eval()>. + +Returns a new C<Template::Document> object or C<undef> on error. The +L<error()|Template::Base#error()> class method can be called, or the C<$ERROR> +package variable inspected to retrieve the relevant error message. + +=head2 process($context) + +Main processing routine for the compiled template document. A reference to a +L<Template::Context> object should be passed as the first parameter. The +method installs any locally defined blocks via a call to the context +L<visit()|Template::Context#visit()> method, processes its own template, +(passing the context reference as a parameter) and then calls +L<leave()|Template::Context#leave()> in the context to allow cleanup. + + print $doc->process($context); + +Returns a text string representing the generated output for the template. +Errors are thrown via C<die()>. + +=head2 block() + +Returns a reference to the main C<BLOCK> subroutine. + +=head2 blocks() + +Returns a reference to the hash array of named C<DEFBLOCKS> subroutines. + +=head2 variables() + +Returns a reference to a hash of variables used in the template. +This requires the L<TRACE_VARS|Template::Manual::Config#TRACE_VARS> +option to be enabled. + +=head2 AUTOLOAD + +An autoload method returns C<METADATA> items. + + print $doc->author(); + +=head1 CLASS METHODS + +These methods are used internally. + +=head2 as_perl($content) + +This method generate a Perl representation of the template. + + my $perl = Template::Document->as_perl({ + BLOCK => $main_block, + DEFBLOCKS => { + foo => $foo_block, + bar => $bar_block, + }, + METADATA => { + name => 'my_template', + } + }); + +=head2 write_perl_file(\%config) + +This method is used to write compiled Perl templates to disk. If the +C<COMPILE_EXT> option (to indicate a file extension for saving compiled +templates) then the L<Template::Parser> module calls this subroutine before +calling the L<new()> constructor. At this stage, the parser has a +representation of the template as text strings containing Perl code. We can +write that to a file, enclosed in a small wrapper which will allow us to +susequently C<require()> the file and have Perl parse and compile it into a +C<Template::Document>. Thus we have persistence of compiled templates. + +=head1 INTERNAL FUNCTIONS + +=head2 catch_warnings() + +This is a simple handler used to catch any errors that arise when the +compiled Perl template is first evaluated (that is, evaluated by Perl to +create a template subroutine at compile, rather than the template being +processed at runtime). + +=head2 is_utf8() + +This is mapped to C<utf8::is_utf8> for versions of Perl that have it (> 5.008) +or to C<Encode::is_utf8> for Perl 5.008. Earlier versions of Perl are not +supported. + +=head1 AUTHOR + +Andy Wardley E<lt>abw@wardley.orgE<gt> L<http://wardley.org/> + +=head1 COPYRIGHT + +Copyright (C) 1996-2012 Andy Wardley. All Rights Reserved. + +This module is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +=head1 SEE ALSO + +L<Template>, L<Template::Parser> + +=cut + +# Local Variables: +# mode: perl +# perl-indent-level: 4 +# indent-tabs-mode: nil +# End: +# +# vim: expandtab shiftwidth=4: diff --git a/perllib/Utils.pm b/perllib/Utils.pm index fa90620a0..7a22c888d 100644 --- a/perllib/Utils.pm +++ b/perllib/Utils.pm @@ -123,7 +123,9 @@ sub london_categories { } sub barnet_categories { - # The values here are KBIDs from Barnet's system: see bin/send-reports for formatting + # The values here are KBIDs from Barnet's system: see bin/send-reports for formatting. + # They are no longer used since Barnet switched to email for delivery of problem reports. + # and can be removed when SendReport/Barnet.pm is removed. if (mySociety::Config::get('STAGING_SITE')) { # note staging site must use different KBIDs return { 'Street scene misc' => 14 # for test @@ -221,18 +223,12 @@ sub cleanup_text { return $input; } -sub prettify_epoch { - my ( $epoch, $type ) = @_; +sub prettify_dt { + my ( $dt, $type ) = @_; $type ||= ''; $type = 'short' if $type eq '1'; - my $dt = DateTime->from_epoch( epoch => $epoch, time_zone => 'local' ); - $dt->set_time_zone( FixMyStreet->config('TIME_ZONE') ) - if FixMyStreet->config('TIME_ZONE'); - - my $now = DateTime->now( time_zone => 'local' ); - $now->set_time_zone( FixMyStreet->config('TIME_ZONE') ) - if FixMyStreet->config('TIME_ZONE'); + my $now = DateTime->now( time_zone => FixMyStreet->config('TIME_ZONE') || 'local' ); my $tt = ''; $tt = $dt->strftime('%H:%M') unless $type eq 'date'; @@ -243,6 +239,8 @@ sub prettify_epoch { $tt .= ', ' unless $type eq 'date'; if ($dt->strftime('%Y %U') eq $now->strftime('%Y %U')) { $tt .= decode_utf8($dt->strftime('%A')); + } elsif ($type eq 'zurich') { + $tt .= decode_utf8($dt->strftime('%e. %B %Y')); } elsif ($type eq 'short') { $tt .= decode_utf8($dt->strftime('%e %b %Y')); } elsif ($dt->strftime('%Y') eq $now->strftime('%Y')) { |