diff options
Diffstat (limited to 'perllib')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin.pm | 37 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Around.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/New.pm | 83 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/Update.pm | 8 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Reports.pm | 15 | ||||
-rwxr-xr-x | perllib/FixMyStreet/App/Controller/Rss.pm | 1 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Tokens.pm | 1 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Default.pm | 8 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Reading.pm | 108 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Contact.pm | 25 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Open311conf.pm | 39 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Problem.pm | 108 | ||||
-rw-r--r-- | perllib/FixMyStreet/Geocode/Bing.pm | 3 | ||||
-rw-r--r-- | perllib/FixMyStreet/Map/FMS.pm | 10 | ||||
-rw-r--r-- | perllib/Open311.pm | 204 | ||||
-rw-r--r-- | perllib/Open311/GetUpdates.pm | 82 | ||||
-rw-r--r-- | perllib/Open311/PopulateServiceList.pm | 240 |
17 files changed, 953 insertions, 21 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index 2aaa488d6..a34737844 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -217,7 +217,10 @@ sub council_list : Path('council_list') : Args(0) { $c->stash->{edit_activity} = $edit_activity; - my @area_types = $c->cobrand->area_types; + # 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 @councils_ids = sort { strcoll($areas->{$a}->{name}, $areas->{$b}->{name}) } keys %$areas; @@ -331,6 +334,32 @@ sub update_contacts : Private { ); $c->stash->{updated} = _('Values updated'); + } elsif ( $posted eq 'open311' ) { + $c->forward('check_token'); + + my %params = map { $_ => $c->req->param($_) } qw/open311_id endpoint jurisdiction api_key area_id/; + + 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->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->insert(); + + $c->stash->{updated} = _('Configuration updated - contacts will be generated automatically later'); + } } } @@ -348,6 +377,12 @@ sub display_contacts : Private { $c->stash->{contacts} = $contacts; + my $open311 = $c->model('DB::Open311Conf')->search( + { area_id => $area_id } + ); + + $c->stash->{open311} = $open311; + if ( $c->req->param('text') && $c->req->param('text') == 1 ) { $c->stash->{template} = 'admin/council_contacts.txt'; $c->res->content_type('text/plain; charset=utf-8'); diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm index 72d6ac62b..148a22368 100644 --- a/perllib/FixMyStreet/App/Controller/Around.pm +++ b/perllib/FixMyStreet/App/Controller/Around.pm @@ -182,7 +182,7 @@ sub display_location : Private { # create a list of all the pins my @pins; - unless ($c->req->param('no_pins')) { + unless ($c->req->param('no_pins') || $c->cobrand->moniker eq 'emptyhomes') { @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 : $_; diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index f16bd5393..e982d6a4c 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -15,6 +15,7 @@ use Path::Class; use Utils; use mySociety::EmailUtil; use mySociety::TempFiles; +use JSON; =head1 NAME @@ -113,11 +114,48 @@ sub report_form_ajax : Path('ajax') : Args(0) { # render templates to get the html my $category = $c->view('Web')->render( $c, 'report/new/category.html'); my $councils_text = $c->view('Web')->render( $c, 'report/new/councils_text.html'); + my $has_open311 = keys %{ $c->stash->{category_extras} }; my $body = JSON->new->utf8(1)->encode( { - councils_text => $councils_text, - category => $category, + councils_text => $councils_text, + category => $category, + has_open311 => $has_open311, + } + ); + + $c->res->content_type('application/json; charset=utf-8'); + $c->res->body($body); +} + +sub category_extras_ajax : Path('category_extras') : Args(0) { + my ( $self, $c ) = @_; + + $c->forward('initialize_report'); + if ( ! $c->forward('determine_location') ) { + my $body = JSON->new->utf8(1)->encode( + { + error => _("Sorry, we could not find that location."), + } + ); + $c->res->content_type('application/json; charset=utf-8'); + $c->res->body($body); + return 1; + } + $c->forward('setup_categories_and_councils'); + + my $category_extra = ''; + if ( $c->stash->{category_extras}->{ $c->req->param('category') } ) { + $c->stash->{report_meta} = {}; + $c->stash->{report} = { category => $c->req->param('category') }; + $c->stash->{category_extras} = { $c->req->param('category' ) => $c->stash->{category_extras}->{ $c->req->param('category') } }; + + $category_extra= $c->view('Web')->render( $c, 'report/new/category_extras.html'); + } + + my $body = JSON->new->utf8(1)->encode( + { + category_extra => $category_extra, } ); @@ -476,6 +514,7 @@ sub setup_categories_and_councils : Private { my %area_ids_to_list = (); # Areas 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 # FIXME - implement in cobrand if ( $c->cobrand->moniker eq 'emptyhomes' ) { @@ -522,8 +561,12 @@ sub setup_categories_and_councils : Private { next if $contact->category eq _('Other'); - push @category_options, $contact->category - unless $seen{$contact->category}; + unless ( $seen{$contact->category} ) { + push @category_options, $contact->category; + + $category_extras{ $contact->category } = $contact->extra + if $contact->extra; + } $seen{$contact->category} = 1; } @@ -538,6 +581,8 @@ sub setup_categories_and_councils : Private { $c->stash->{area_ids_to_list} = [ keys %area_ids_to_list ]; $c->stash->{category_label} = $category_label; $c->stash->{category_options} = \@category_options; + $c->stash->{category_extras} = \%category_extras; + $c->stash->{category_extras_json} = encode_json \%category_extras; my @missing_details_councils = grep { !$area_ids_to_list{$_} } # @@ -716,6 +761,26 @@ sub process_report : Private { if $council_string && @{ $c->stash->{missing_details_councils} }; $report->council($council_string); + my @extra = (); + my $metas = $contacts[0]->extra; + + foreach my $field ( @$metas ) { + if ( lc( $field->{required} ) eq 'true' ) { + unless ( $c->request->param( $field->{code} ) ) { + $c->stash->{field_errors}->{ $field->{code} } = _('This information is required'); + } + } + push @extra, { + name => $field->{code}, + description => $field->{description}, + value => $c->request->param( $field->{code} ) || '', + }; + } + + if ( @extra ) { + $c->stash->{report_meta} = \@extra; + $report->extra( \@extra ); + } } elsif ( @{ $c->stash->{area_ids_to_list} } ) { # There was an area with categories, but we've not been given one. Bail. @@ -881,6 +946,16 @@ sub save_user_and_report : Private { # Save or update the user if appropriate if ( !$report->user->in_storage ) { + # User does not exist. + # Store changes in token for when token is validated. + $c->stash->{token_data} = { + name => $report->user->name, + phone => $report->user->phone, + password => $report->user->password, + }; + $report->user->name( undef ); + $report->user->phone( undef ); + $report->user->password( '', 1 ); $report->user->insert(); } elsif ( $c->user && $report->user->id == $c->user->id ) { diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index add9d1371..c67ca4d1f 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -255,6 +255,14 @@ sub save_update : Private { my $update = $c->stash->{update}; if ( !$update->user->in_storage ) { + # User does not exist. + # Store changes in token for when token is validated. + $c->stash->{token_data} = { + name => $update->user->name, + password => $update->user->password, + }; + $update->user->name( undef ); + $update->user->password( '', 1 ); $update->user->insert; } elsif ( $c->user && $c->user->id == $update->user->id ) { diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm index 93af3393c..0587a627a 100644 --- a/perllib/FixMyStreet/App/Controller/Reports.pm +++ b/perllib/FixMyStreet/App/Controller/Reports.pm @@ -110,6 +110,8 @@ sub ward : Path : Args(2) { $c->stash->{council_url} = '/reports/' . $council_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 @@ -338,7 +340,7 @@ sub load_and_group_problems : Private { $where, { columns => [ - 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', + 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', 'cobrand', { duration => { extract => "epoch from current_timestamp-lastupdate" } }, { age => { extract => "epoch from current_timestamp-confirmed" } }, ], @@ -351,9 +353,10 @@ sub load_and_group_problems : Private { my ( %fixed, %open, @pins ); my $re_councils = join('|', keys %{$c->stash->{areas_info}}); - my @cols = ( 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', 'duration', 'age' ); + my @cols = ( 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', 'cobrand', 'duration', 'age' ); while ( my @problem = $problems->next ) { my %problem = zip @cols, @problem; + $c->log->debug( $problem{'cobrand'} . ', cobrand is ' . $c->cobrand->moniker ); if ( !$problem{council} ) { # Problem was not sent to any council, add to possible councils $problem{councils} = 0; @@ -372,10 +375,10 @@ sub load_and_group_problems : Private { } } - $c->stash( - fixed => \%fixed, - open => \%open, - pins => \@pins, + $c->stash( + fixed => \%fixed, + open => \%open, + pins => \@pins, ); return 1; diff --git a/perllib/FixMyStreet/App/Controller/Rss.pm b/perllib/FixMyStreet/App/Controller/Rss.pm index 23345df65..7fddbed97 100755 --- a/perllib/FixMyStreet/App/Controller/Rss.pm +++ b/perllib/FixMyStreet/App/Controller/Rss.pm @@ -151,6 +151,7 @@ sub local_problems_ll : Private { sub output : Private { my ( $self, $c ) = @_; + $c->detach( '/page_error_404_not_found', [ 'Feed not found' ] ) if $c->cobrand->moniker eq 'emptyhomes'; $c->forward( 'lookup_type' ); $c->forward( 'query_main' ); $c->forward( 'generate' ); diff --git a/perllib/FixMyStreet/App/Controller/Tokens.pm b/perllib/FixMyStreet/App/Controller/Tokens.pm index 10f994d9f..b974f94e6 100644 --- a/perllib/FixMyStreet/App/Controller/Tokens.pm +++ b/perllib/FixMyStreet/App/Controller/Tokens.pm @@ -69,6 +69,7 @@ sub confirm_problem : Path('/P') { # log the problem creation user in to the site 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->password( $data->{password}, 1 ) if $data->{password}; $problem->user->update; } diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 1e87468ac..2900497c4 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -960,5 +960,13 @@ to be resized then return 0; sub default_photo_resize { return 0; } +=head2 get_report_stats + +Get stats to display on the council reports page + +=cut + +sub get_report_stats { return 0; } + 1; diff --git a/perllib/FixMyStreet/Cobrand/Reading.pm b/perllib/FixMyStreet/Cobrand/Reading.pm new file mode 100644 index 000000000..8e98931fd --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Reading.pm @@ -0,0 +1,108 @@ +package FixMyStreet::Cobrand::Reading; +use base 'FixMyStreet::Cobrand::Default'; + +use strict; +use warnings; + +use Carp; +use URI::Escape; +use mySociety::VotingArea; + +sub site_restriction { + return ( "and council='2596'", 'reading', { council => '2596' } ); +} + +sub problems_clause { + return { council => '2596' }; +} + +sub problems { + my $self = shift; + return $self->{c}->model('DB::Problem')->search( $self->problems_clause ); +} + +sub base_url { + my $base_url = mySociety::Config::get('BASE_URL'); + if ($base_url !~ /reading/) { + $base_url =~ s{http://(?!www\.)}{http://reading.}g; + $base_url =~ s{http://www\.}{http://reading.}g; + } + return $base_url; +} + +sub site_title { + my ( $self ) = @_; + return 'Reading City Council FixMyStreet'; +} + +sub enter_postcode_text { + my ( $self ) = @_; + return 'Enter a Reading postcode, or street name and area'; +} + +sub council_check { + my ( $self, $params, $context ) = @_; + + my $councils = $params->{all_councils}; + my $council_match = defined $councils->{2596}; + if ($council_match) { + return 1; + } + my $url = 'http://www.fixmystreet.com/'; + $url .= 'alert' if $context eq 'alert'; + $url .= '?pc=' . URI::Escape::uri_escape_utf8($self->{c}->req->param('pc')) + if $self->{c}->req->param('pc'); + my $error_msg = "That location is not covered by Reading. +Please visit <a href=\"$url\">the main FixMyStreet site</a>."; + return ( 0, $error_msg ); +} + +# All reports page only has the one council. +sub all_councils_report { + return 0; +} + +sub disambiguate_location { + return { + town => 'Reading', + centre => '51.452983169803964,-0.98382678731985973', + span => '0.0833543573028663,0.124500468843446', + bounds => [ '51.409779668156361,-1.0529948144525243', '51.493134025459227,-0.92849434560907829' ], + }; +} + +sub recent_photos { + my ($self, $num, $lat, $lon, $dist) = @_; + $num = 2 if $num == 3; + return $self->problems->recent_photos( $num, $lat, $lon, $dist ); +} + +sub get_report_stats { + my $self = shift; + + my ( $cobrand, $main_site ) = ( 0, 0 ); + + $self->{c}->log->debug( 'X' x 60 ); + my $stats = $self->{c}->model('DB::Problem')->search( + { confirmed => { '>=', '2011-11-01' } }, + { + select => [ { count => 'id', -as => 'cobrand_count' }, 'cobrand' ], + group_by => [qw/cobrand/] + } + ); + + while ( my $stat = $stats->next ) { + if ( $stat->cobrand eq $self->moniker ) { + $cobrand += $stat->get_column( 'cobrand_count' ); + } else { + $main_site += $stat->get_column( 'cobrand_count' ); + } + } + + return { + cobrand => $cobrand, + main_site => $main_site, + }; +} + +1; diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm index 001fb4ac6..941e4e1bb 100644 --- a/perllib/FixMyStreet/DB/Result/Contact.pm +++ b/perllib/FixMyStreet/DB/Result/Contact.pm @@ -34,12 +34,33 @@ __PACKAGE__->add_columns( { data_type => "timestamp", is_nullable => 0 }, "note", { data_type => "text", is_nullable => 0 }, + "extra", + { data_type => "text", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("contacts_area_id_category_idx", ["area_id", "category"]); +# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-08-01 10:07:59 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4y6yRz4rMN66pBpkzfJJhg -# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-06-23 15:49:48 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BXGd4uk1ybC5RTKlInTr0w +__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; + }, + } +); 1; diff --git a/perllib/FixMyStreet/DB/Result/Open311conf.pm b/perllib/FixMyStreet/DB/Result/Open311conf.pm new file mode 100644 index 000000000..0a5784560 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/Open311conf.pm @@ -0,0 +1,39 @@ +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 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint("open311conf_area_id_key", ["area_id"]); + + +# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-07-29 18:09:25 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ryqCpvwjNtQrZm4I3s0hxg + + +# 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 987c92c64..9ff19efb6 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -78,6 +78,8 @@ __PACKAGE__->add_columns( { data_type => "timestamp", is_nullable => 1 }, "send_questionnaire", { data_type => "boolean", default_value => \"true", is_nullable => 0 }, + "extra", + { data_type => "text", is_nullable => 1 }, "flagged", { data_type => "boolean", default_value => \"false", is_nullable => 0 }, ); @@ -102,8 +104,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-06-23 15:49:48 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3sw/1dqxlTvcWEI/eJTm4w +# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-07-29 16:26:23 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ifvx9FOlbui66hPyzNIAPA # Add fake relationship to stored procedure table __PACKAGE__->has_one( @@ -113,11 +115,31 @@ __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; + }, + } +); use DateTime::TimeZone; use Image::Size; use Moose; use namespace::clean -except => [ 'meta' ]; use Utils; +use RABX; with 'FixMyStreet::Roles::Abuser'; @@ -535,6 +557,88 @@ sub duration_string { ); } +=head2 update_from_open311_service_request + + $p->update_from_open311_service_request( $request, $council_details, $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. + +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. + +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 ( $updated, $status_notes ); + + if ( ! ref $request->{updated_datetime} ) { + $updated = $request->{updated_datetime}; + } + + if ( ! ref $request->{status_notes} ) { + $status_notes = $request->{status_notes}; + } + + my $update = FixMyStreet::App->model('DB::Comment')->new( + { + problem_id => $self->id, + state => 'confirmed', + created => $updated || \'ms_current_timestamp()', + confirmed => \'ms_current_timestamp()', + text => $status_notes, + mark_open => 0, + mark_fixed => 0, + user => $system_user, + anonymous => 0, + name => $council_details->{name}, + } + ); + + + my $w3c = DateTime::Format::W3CDTF->new; + my $req_time = $w3c->parse_datetime( $request->{updated_datetime} ); + + # set a timezone here as the $req_time will have one and if we don't + # use a timezone then the date comparisons are invalid. + # of course if local timezone is not the one that went into the data + # base then we're also in trouble + my $lastupdate = $self->lastupdate; + $lastupdate->set_time_zone( DateTime::TimeZone->new( name => 'local' ) ); + + # update from open311 is older so skip + if ( $req_time < $lastupdate ) { + return 0; + } + + if ( $request->{status} eq 'closed' ) { + if ( $self->state ne 'fixed' ) { + $self->state('fixed'); + $update->mark_fixed(1); + + if ( !$status_notes ) { + # FIXME - better text here + $status_notes = _('Closed by council'); + } + } + } + + if ( $status_notes ) { + $update->text( $status_notes ); + $self->lastupdate( $req_time ); + $self->update; + $update->insert; + } + + return 1; +} + # 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/Geocode/Bing.pm b/perllib/FixMyStreet/Geocode/Bing.pm index 4e12a7a7f..856d7061e 100644 --- a/perllib/FixMyStreet/Geocode/Bing.pm +++ b/perllib/FixMyStreet/Geocode/Bing.pm @@ -22,6 +22,7 @@ use Digest::MD5 qw(md5_hex); # may be used to disambiguate the location in cobranded versions of the site. sub string { my ( $s, $c, $params ) = @_; + $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; my $url = "http://dev.virtualearth.net/REST/v1/Locations?q=$s&c=en-GB"; # FIXME nb-NO for Norway $url .= '&mapView=' . $params->{bounds}[0] . ',' . $params->{bounds}[1] if $params->{bounds}; @@ -43,7 +44,7 @@ sub string { if (!$js) { return { error => _('Sorry, we could not parse that location. Please try again.') }; } elsif ($js =~ /BT\d/) { - return { error => _("We do not cover Northern Ireland, I'm afraid, as our licence doesn't include any maps for the region.") }; + return { error => _("We do not currently cover Northern Ireland, I'm afraid.") }; } $js = JSON->new->utf8->allow_nonref->decode($js); diff --git a/perllib/FixMyStreet/Map/FMS.pm b/perllib/FixMyStreet/Map/FMS.pm index d5edac763..24842c861 100644 --- a/perllib/FixMyStreet/Map/FMS.pm +++ b/perllib/FixMyStreet/Map/FMS.pm @@ -47,11 +47,13 @@ sub map_tiles { "http://tilma.mysociety.org/sv/$z/$x/$y.png", ]; } else { + my $url = "g=701"; + $url .= "&productSet=mmOS" if $z > 10; return [ - "http://ecn.t0.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y-1, $z) . ".png?g=701&productSet=mmOS", - "http://ecn.t1.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y-1, $z) . ".png?g=701&productSet=mmOS", - "http://ecn.t2.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y, $z) . ".png?g=701&productSet=mmOS", - "http://ecn.t3.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y, $z) . ".png?g=701&productSet=mmOS", + "http://ecn.t0.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y-1, $z) . ".png?$url", + "http://ecn.t1.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y-1, $z) . ".png?$url", + "http://ecn.t2.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y, $z) . ".png?$url", + "http://ecn.t3.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y, $z) . ".png?$url", ]; } } diff --git a/perllib/Open311.pm b/perllib/Open311.pm new file mode 100644 index 000000000..f3f642895 --- /dev/null +++ b/perllib/Open311.pm @@ -0,0 +1,204 @@ +package Open311; + +use URI; +use Moose; +use XML::Simple; +use LWP::Simple; +use LWP::UserAgent; +use HTTP::Request::Common qw(POST); + +has jurisdiction => ( is => 'ro', isa => 'Str' );; +has api_key => ( is => 'ro', isa => 'Str' ); +has endpoint => ( is => 'ro', isa => 'Str' ); +has test_mode => ( is => 'ro', isa => 'Bool' ); +has test_uri_used => ( is => 'rw', 'isa' => 'Str' ); +has test_get_returns => ( is => 'rw' ); +has endpoints => ( is => 'rw', default => sub { { services => 'services.xml', requests => 'requests.xml' } } ); + +sub get_service_list { + my $self = shift; + + my $service_list_xml = $self->_get( $self->endpoints->{services} ); + + return $self->_get_xml_object( $service_list_xml ); +} + +sub get_service_meta_info { + my $self = shift; + my $service_id = shift; + + my $service_meta_xml = $self->_get( "services/$service_id.xml" ); + return $self->_get_xml_object( $service_meta_xml ); +} + +sub send_service_request { + my $self = shift; + my $problem = shift; + my $extra = shift; + my $service_code = shift; + + my $description = <<EOT; +title: @{[$problem->title()]} + +detail: @{[$problem->detail()]} + +url: $extra->{url} + +Submitted via FixMyStreet +EOT +; + + my $params = { + lat => $problem->latitude, + long => $problem->longitude, + email => $problem->user->email, + description => $description, + service_code => $service_code, + }; + + if ( $problem->user->phone ) { + $params->{ phone } = $problem->user->phone; + } + + if ( $extra->{image_url} ) { + $params->{media_url} = $extra->{image_url}; + } + + if ( $problem->extra ) { + my $extras = $problem->extra; + + for my $attr ( @$extras ) { + my $name = sprintf( 'attribute[%s]', $attr->{name} ); + $params->{ $name } = $attr->{value}; + } + } + + my $response = $self->_post( $self->endpoints->{requests}, $params ); + + if ( $response ) { + my $obj = $self->_get_xml_object( $response ); + + if ( $obj ) { + if ( $obj->{ request }->{ service_request_id } ) { + return $obj->{ request }->{ service_request_id }; + } else { + my $token = $obj->{ request }->{ token }; + if ( $token ) { + return $self->get_service_request_id_from_token( $token ); + } + } + } + + warn sprintf( "Failed to submit problem %s over Open311, response\n: %s", $problem->id, $response ); + return 0; + } +} + +sub get_service_requests { + my $self = shift; + my $report_ids = shift; + + my $params = {}; + + if ( $report_ids ) { + $params->{service_request_id} = join ',', @$report_ids; + } + + my $service_request_xml = $self->_get( $self->endpoints->{requests}, $params || undef ); + return $self->_get_xml_object( $service_request_xml ); +} + +sub get_service_request_id_from_token { + my $self = shift; + my $token = shift; + + my $service_token_xml = $self->_get( "tokens/$token.xml" ); + + my $obj = $self->_get_xml_object( $service_token_xml ); + + if ( $obj && $obj->{ request }->{ service_request_id } ) { + return $obj->{ request }->{ service_request_id }; + } else { + return 0; + } +} + +sub _get { + my $self = shift; + my $path = shift; + my $params = shift || {}; + + my $uri = URI->new( $self->endpoint ); + + $params->{ jurisdiction_id } = $self->jurisdiction; + $uri->path( $uri->path . $path ); + $uri->query_form( $params ); + + my $content; + if ( $self->test_mode ) { + $content = $self->test_get_returns->{ $path }; + $self->test_uri_used( $uri->as_string ); + } else { + $content = get( $uri->as_string ); + } + + return $content; +} + +sub _post { + my $self = shift; + my $path = shift; + my $params = shift; + + my $uri = URI->new( $self->endpoint ); + $uri->path( $uri->path . $path ); + + my $req = POST $uri->as_string, + [ + jurisdiction_id => $self->jurisdiction, + api_key => $self->api_key, + %{ $params } + ]; + + my $ua = LWP::UserAgent->new(); + my $res = $ua->request( $req ); + + if ( $res->is_success ) { + return $res->decoded_content; + } else { + warn "request failed: " . $res->status_line; + warn $self->_process_error( $res->decoded_content ); + return 0; + } +} + +sub _process_error { + my $self = shift; + my $error = shift; + + my $obj = $self->_get_xml_object( $error ); + + my $msg = ''; + if ( ref $obj && exists $obj->{error} ) { + my $errors = $obj->{error}; + $errors = [ $errors ] if ref $errors ne 'ARRAY'; + $msg .= sprintf( "%s: %s\n", $_->{code}, $_->{description} ) for @{ $errors }; + } + + return $msg || 'unknown error'; +} + +sub _get_xml_object { + my $self = shift; + my $xml= shift; + + my $simple = XML::Simple->new(); + my $obj; + + eval { + $obj = $simple ->XMLin( $xml ); + }; + + return $obj; +} +1; diff --git a/perllib/Open311/GetUpdates.pm b/perllib/Open311/GetUpdates.pm new file mode 100644 index 000000000..5d5291d47 --- /dev/null +++ b/perllib/Open311/GetUpdates.pm @@ -0,0 +1,82 @@ +package Open311::GetUpdates; + +use Moose; +use Open311; +use FixMyStreet::App; + +has council_list => ( is => 'ro' ); +has system_user => ( is => 'ro' ); + +sub get_updates { + my $self = shift; + + while ( my $council = $self->council_list->next ) { + my $open311 = Open311->new( + endpoint => $council->endpoint, + jurisdiction => $council->jurisdiction, + api_key => $council->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\%" }, + state => { 'IN', [qw/confirmed fixed/] }, + -and => [ + external_id => { '!=', undef }, + external_id => { '!=', '' }, + ], + } + ); + + my @report_ids = (); + while ( my $report = $reports->next ) { + push @report_ids, $report->external_id; + } + + next unless @report_ids; + + $self->update_reports( \@report_ids, $open311, $council_details ); + } +} + +sub update_reports { + my ( $self, $report_ids, $open311, $council_details ) = @_; + + my $service_requests = $open311->get_service_requests( $report_ids ); + + my $requests; + + # XML::Simple is a bit inconsistent in how it structures + # things depending on the number of children an element has :( + if ( ref $service_requests->{request} eq 'ARRAY' ) { + $requests = $service_requests->{request}; + } + else { + $requests = [ $service_requests->{request} ]; + } + + for my $request (@$requests) { + # if it's a ref that means it's an empty element + # however, if there's no updated date then we can't + # tell if it's newer that what we have so we should skip it + next if ref $request->{updated_datetime} || ! exists $request->{updated_datetime}; + + my $request_id = $request->{service_request_id}; + + my $problem = + FixMyStreet::App->model('DB::Problem') + ->search( { external_id => $request_id, } ); + + if (my $p = $problem->first) { + $p->update_from_open311_service_request( $request, $council_details, $self->system_user ); + } + } + + return 1; +} + +1; diff --git a/perllib/Open311/PopulateServiceList.pm b/perllib/Open311/PopulateServiceList.pm new file mode 100644 index 000000000..cfec9005d --- /dev/null +++ b/perllib/Open311/PopulateServiceList.pm @@ -0,0 +1,240 @@ +package Open311::PopulateServiceList; + +use Moose; +use LWP::Simple; +use XML::Simple; +use FixMyStreet::App; +use Open311; + +has council_list => ( is => 'ro' ); +has found_contacts => ( is => 'rw', default => sub { [] } ); + +has _current_council => ( is => 'rw' ); +has _current_open311 => ( is => 'rw' ); +has _current_service => ( is => 'rw' ); + +my $council_list = FixMyStreet::App->model('DB::Open311conf'); + +sub process_councils { + my $self = shift; + + while ( my $council = $self->council_list->next ) { + next unless $council->endpoint; + $self->_current_council( $council ); + $self->process_council; + } +} + +sub process_council { + my $self = shift; + + my $open311 = Open311->new( + endpoint => $self->_current_council->endpoint, + jurisdiction => $self->_current_council->jurisdiction, + api_key => $self->_current_council->api_key + ); + + $self->_current_open311( $open311 ); + $self->_check_endpoints; + + my $list = $open311->get_service_list; + unless ( $list ) { + warn "ERROR: no service list found for " . $self->_current_council->area_id . "\n"; + return; + } + $self->process_services( $list ); +} + + + +sub _check_endpoints { + my $self = shift; + + # west berks end point not standard + if ( $self->_current_council->area_id == 2619 ) { + $self->_current_open311->endpoints( + { + services => 'Services', + requests => 'Requests' + } + ); + } +} + + +sub process_services { + my $self = shift; + my $list = shift; + + $self->found_contacts( [] ); + foreach my $service ( @{ $list->{service} } ) { + $self->_current_service( $service ); + $self->process_service; + } + $self->_delete_contacts_not_in_service_list; +} + +sub process_service { + my $self = shift; + + my $category = $self->_current_council->area_id == 2218 ? + $self->_current_service->{description} : + $self->_current_service->{service_name}; + + print $self->_current_service->{service_code} . ': ' . $category . "\n"; + my $contacts = FixMyStreet::App->model( 'DB::Contact')->search( + { + area_id => $self->_current_council->area_id, + -OR => [ + email => $self->_current_service->{service_code}, + category => $category, + ] + } + ); + + if ( $contacts->count() > 1 ) { + printf( + "Multiple contacts for service code %s, category %s - Skipping\n", + $self->_current_service->{service_code}, + $category, + ); + + # best to not mark them as deleted as we don't know what we're doing + while ( my $contact = $contacts->next ) { + push @{ $self->found_contacts }, $contact->email; + } + + return; + } + + my $contact = $contacts->first; + + if ( $contact ) { + $self->_handle_existing_contact( $contact ); + } else { + $self->_create_contact; + } +} + +sub _handle_existing_contact { + my ( $self, $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 ( $contact->deleted || $service_name ne $contact->category || $self->_current_service->{service_code} ne $contact->email ) { + eval { + $contact->update( + { + category => $service_name, + email => $self->_current_service->{service_code}, + confirmed => 1, + deleted => 0, + editor => $0, + whenedited => \'ms_current_timestamp()', + note => 'automatically undeleted by script', + } + ); + }; + + if ( $@ ) { + warn "Failed to update contact for service code " . $self->_current_service->{service_code} . " for council @{[$self->_current_council->area_id]}: $@\n"; + return; + } + } + + push @{ $self->found_contacts }, $self->_current_service->{service_code}; +} + +sub _create_contact { + my $self = shift; + + my $service_name = $self->_normalize_service_name; + + my $contact; + eval { + $contact = FixMyStreet::App->model( 'DB::Contact')->create( + { + email => $self->_current_service->{service_code}, + area_id => $self->_current_council->area_id, + category => $service_name, + confirmed => 1, + deleted => 0, + editor => $0, + whenedited => \'ms_current_timestamp()', + note => 'created automatically by script', + } + ); + }; + + if ( $@ ) { + warn "Failed to create contact for service code " . $self->_current_service->{service_code} . " for council @{[$self->_current_council->area_id]}: $@\n"; + return; + } + + if ( $contact and lc( $self->_current_service->{metadata} ) eq 'true' ) { + $self->_add_meta_to_contact( $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"; + } +} + +sub _add_contact_to_meta { + my ( $self, $contact ) = @_; + + print "Fetching meta data for $self->_current_service->{service_code}\n"; + my $meta_data = $self->_current_open311->get_service_meta_info( $self->_current_service->{service_code} ); + + # turn the data into something a bit more friendly to use + my @meta = + # remove trailing colon as we add this when we display so we don't want 2 + map { $_->{description} =~ s/:\s*//; $_ } + # there is a display order and we only want to sort once + sort { $a->{order} <=> $b->{order} } + @{ $meta_data->{attributes}->{attribute} }; + + $contact->extra( \@meta ); + $contact->update; +} + +sub _normalize_service_name { + my $self = shift; + + # 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} : + $self->_current_service->{service_name}; + # remove trailing whitespace as it upsets db queries + # to look up contact details when creating problem + $service_name =~ s/\s+$//; + + return $service_name; +} + +sub _delete_contacts_not_in_service_list { + my $self = shift; + + my $found_contacts = FixMyStreet::App->model( 'DB::Contact')->search( + { + email => { -not_in => $self->found_contacts }, + area_id => $self->_current_council->area_id, + deleted => 0, + } + ); + + $found_contacts->update( + { + deleted => 1, + editor => $0, + whenedited => \'ms_current_timestamp()', + note => 'automatically marked as deleted by script' + } + ); +} + +1; |