diff options
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Open311.pm')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Open311.pm | 458 |
1 files changed, 458 insertions, 0 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Open311.pm b/perllib/FixMyStreet/App/Controller/Open311.pm new file mode 100644 index 000000000..459ce12c9 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Open311.pm @@ -0,0 +1,458 @@ +package FixMyStreet::App::Controller::Open311; + +use utf8; +use Moose; +use namespace::autoclean; + +use JSON; +use XML::Simple; +use DateTime::Format::W3CDTF; + +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Open311 - Catalyst Controller + +=head1 DESCRIPTION + +Open311 server API + +Open311 server API for Open311 clients + +http://open311.org/ +http://wiki.open311.org/GeoReport_v2 +http://fixmystreet.org.nz/api +http://seeclickfix.com/open311/ + +Issues with Open311 + * no way to specify which languages are understood by the + recipients. some lang=nb,nn setting should be available. + * not obvious how to handle generic requests (ie without lat/lon + values). + * should service IDs be numeric or not? Spec do not say, and all + examples I find use numbers. + * missing way to search for reports near a location using lat/lon + * report attributes lack title field. + * missing way to provide updates information for a request + * should support GeoRSS output as well as json and home made XML + +=head1 METHODS + +=cut + +=head2 index + +Displays some summary information for the requests. + +=cut + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + # don't need to do anything here - should just pass through. +} + +sub old_uri : Regex('^open311\.cgi') : Args(0) { + my ( $self, $c ) = @_; + ( my $new = $c->req->path ) =~ s/open311.cgi/open311/; + $c->res->redirect( "/$new", 301); +} + +=head2 discovery + +http://search.cpan.org/~bobtfish/Catalyst-Manual-5.8007/lib/Catalyst/Manual/Intro.pod + +=cut + +sub discovery_v2 : LocalRegex('^v2/discovery.(xml|json|html)$') : Args(0) { + my ( $self, $c ) = @_; + $c->stash->{format} = $c->req->captures->[0]; + $c->forward( 'get_discovery' ); +} + +sub services_v2 : LocalRegex('^v2/services.(xml|json|html)$') : Args(0) { + my ( $self, $c ) = @_; + $c->stash->{format} = $c->req->captures->[0]; + $c->forward( 'get_services' ); +} + +sub requests_v2 : LocalRegex('^v2/requests.(xml|json|html|rss)$') : Args(0) { + my ( $self, $c ) = @_; + $c->stash->{format} = $c->req->captures->[0]; + $c->forward( 'get_requests' ); +} + +sub request_v2 : LocalRegex('^v2/requests/(\d+).(xml|json|html)$') : Args(0) { + my ( $self, $c ) = @_; + $c->stash->{id} = $c->req->captures->[0]; + $c->stash->{format} = $c->req->captures->[1]; + $c->forward( 'get_request' ); +} + +sub error : Private { + my ( $self, $c, $error ) = @_; + $c->stash->{error} = "ERROR: $error"; + $c->stash->{template} = 'open311/index.html'; +} + +# Example +# http://sandbox.georeport.org/tools/discovery/discovery.xml +sub get_discovery : Private { + my ( $self, $c ) = @_; + + my $contact_email = $c->config->{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'; + my $test_changeset = $prod_changeset; + my $spec_url = 'http://wiki.open311.org/GeoReport_v2'; + my $info = + { + 'contact' => ["Send email to $contact_email."], + 'changeset' => [$prod_changeset], + # XXX rewrite to match + 'key_service' => ["Read access is open to all according to our \u003Ca href='/open_data' target='_blank'\u003Eopen data license\u003C/a\u003E. For write access either: 1. return the 'guid' cookie on each call (unique to each client) or 2. use an api key from a user account which can be generated here: http://seeclickfix.com/register The unversioned url will always point to the latest supported version."], + 'max_requests' => [ $c->config->{RSS_LIMIT} ], + 'endpoints' => [ + { + 'endpoint' => [ + { + 'formats' => [ + {'format' => [ 'text/xml', + 'application/json', + 'text/html' ] + } + ], + 'specification' => [ $spec_url ], + 'changeset' => [ $prod_changeset ], + 'url' => [ $prod_url ], + 'type' => [ 'production' ] + }, + { + 'formats' => [ + { + 'format' => [ 'text/xml', + 'application/json', + 'text/html' ] + } + ], + 'specification' => [ $spec_url ], + 'changeset' => [ $test_changeset ], + 'url' => [ $test_url ], + 'type' => [ 'test' ] + } + ] + } + ] + }; + $c->forward( 'format_output', [ { + 'discovery' => $info + } ] ); +} + +# Example +# http://seeclickfix.com/open311/services.html?lat=32.1562864999991&lng=-110.883806 +sub get_services : Private { + my ( $self, $c ) = @_; + + my $jurisdiction_id = $c->req->param('jurisdiction_id') || ''; + my $lat = $c->req->param('lat') || ''; + my $lon = $c->req->param('long') || ''; + + # Look up categories for this council or councils + my $categories = $c->model('DB::Contact')->not_deleted; + + if ($lat || $lon) { + my @area_types = $c->cobrand->area_types; + my $all_councils = mySociety::MaPit::call('point', + "4326/$lon,$lat", + type => \@area_types); + $categories = $categories->search( { + area_id => [ keys %$all_councils ], + } ); + } + + my @categories = $categories->search( undef, { + columns => [ 'category' ], + distinct => 1, + } )->all; + + my @services; + for my $categoryref ( sort { $a->category cmp $b->category } + @categories) { + my $categoryname = $categoryref->category; + push(@services, + { + # FIXME Open311 v2 seem to require all three, and we + # only have one value. + 'service_name' => [ $categoryname ], + 'description' => [ $categoryname ], + 'service_code' => [ $categoryname ], + 'metadata' => [ 'false' ], + 'type' => [ 'realtime' ], +# 'group' => [ '' ], +# 'keywords' => [ '' ], + } + ); + } + $c->forward( 'format_output', [ { + 'services' => [ { + 'service' => \@services + } ] + } ] ); +} + + +sub output_requests : Private { + my ( $self, $c, $criteria, $limit ) = @_; + $limit = $c->config->{RSS_LIMIT} + unless $limit && $limit <= $c->config->{RSS_LIMIT}; + + my $attr = { + order_by => { -desc => 'confirmed' }, + rows => $limit + }; + + # Look up categories for this council or councils + my $problems = $c->cobrand->problems->search( $criteria, $attr ); + + my %statusmap = ( 'fixed' => 'closed', + 'confirmed' => 'open'); + + 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 = + { + 'service_request_id' => [ $id ], + 'title' => [ $problem->title ], # Not in Open311 v2 + 'detail' => [ $problem->detail ], # Not in Open311 v2 + 'description' => [ $problem->title .': ' . $problem->detail ], + 'lat' => [ $problem->latitude ], + 'long' => [ $problem->longitude ], + 'status' => [ $problem->state ], +# 'status_notes' => [ {} ], + 'requested_datetime' => [ w3date($problem->confirmed_local) ], + 'updated_datetime' => [ w3date($problem->lastupdate_local) ], +# '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 ( !$problem->anonymous ) { + # Not in Open311 v2 + $request->{'requestor_name'} = [ $problem->name ]; + } + if ( $problem->whensent ) { + # Not in Open311 v2 + $request->{'agency_sent_datetime'} = + [ w3date($problem->whensent_local) ]; + } + + # Extract number of updates + my $updates = $problem->comments->search( + { state => 'confirmed' }, + )->count; + if ($updates) { + # Not in Open311 v2 + $request->{'comment_count'} = [ $updates ]; + } + + my $display_photos = $c->cobrand->allow_photo_display; + if ($display_photos && $problem->photo) { + my $url = $c->cobrand->base_url(); + my $imgurl = $url . "/photo?id=$id"; + $request->{'media_url'} = [ $imgurl ]; + } + 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}} ; + $request->{agency_responsible} = + [ {'recipient' => [ @council_names ] } ]; + } + } + $c->forward( 'format_output', [ { + 'requests' => [ { + 'request' => \@problemlist + } ] + } ] ); +} + +sub get_requests : Private { + my ( $self, $c ) = @_; + + $c->forward( 'is_jurisdiction_id_ok' ); + + my $max_requests = $c->req->param('max_requests') || 0; + + # Only provide access to the published reports + my $criteria = { + state => [ 'fixed', 'confirmed' ] + }; + + my %rules = ( + service_request_id => [ '=', 'id' ], + service_code => [ '=', 'category' ], + status => [ '=', 'state' ], + start_date => [ '>=', 'confirmed' ], + end_date => [ '<', 'confirmed' ], + agency_responsible => [ '~', 'council' ], + interface_used => [ '=', 'service' ], + has_photo => [ '=', 'photo' ], + ); + for my $param (keys %rules) { + my $value = $c->req->param($param); + next unless $value; + my $op = $rules{$param}[0]; + my $key = $rules{$param}[1]; + if ( 'status' eq $param ) { + $value = { + 'open' => 'confirmed', + 'closed' => 'fixed' + }->{$value}; + } elsif ( 'agency_responsible' eq $param ) { + my @valuelist; + for my $agency (split(/\|/, $value)) { + unless ($agency =~ m/^(\d+)$/) { + $c->detach( 'error', [ + sprintf(_('Invalid agency_responsible value %s'), + $value) + ] ); + } + my $agencyid = $1; + # FIXME This seem to match the wrong entries + # some times. Not sure when or why + my $re = "(\\y$agencyid\\y|^$agencyid\\y|\\y$agencyid\$)"; + push(@valuelist, $re); + } + $value = \@valuelist; + } elsif ( 'has_photo' eq $param ) { + $value = undef; + $op = '!=' if 'true' eq $value; + $c->detach( 'error', [ + sprintf(_('Incorrect has_photo value "%s"'), + $value) + ] ) + unless 'true' eq $value || 'false' eq $value; + } elsif ( 'interface_used' eq $param ) { + $value = undef if 'Web interface' eq $value; + } + $criteria->{$key} = { $op, $value }; + } + + if ('rss' eq $c->stash->{format}) { + $c->stash->{type} = 'new_problems'; + $c->forward( '/rss/lookup_type' ); + $c->forward( 'rss_query', [ $criteria, $max_requests ] ); + $c->forward( '/rss/generate' ); + } else { + $c->forward( 'output_requests', [ $criteria, $max_requests ] ); + } +} + +sub rss_query : Private { + my ( $self, $c, $criteria, $limit ) = @_; + $limit = $c->config->{RSS_LIMIT} + unless $limit && $limit <= $c->config->{RSS_LIMIT}; + + my $attr = { + result_class => 'DBIx::Class::ResultClass::HashRefInflator', + order_by => { -desc => 'confirmed' }, + rows => $limit + }; + + my $problems = $c->cobrand->problems->search( $criteria, $attr ); + $c->stash->{problems} = $problems; +} + +# Example +# http://seeclickfix.com/open311/requests/1.xml?jurisdiction_id=sfgov.org +sub get_request : Private { + my ( $self, $c ) = @_; + my $format = $c->stash->{format}; + my $id = $c->stash->{id}; + + $c->forward( 'is_jurisdiction_id_ok' ); + + if ('html' eq $format) { + my $base_url = $c->cobrand->base_url(); + $c->res->redirect($base_url . "/report/$id"); + return; + } + + my $criteria = { + state => [ 'fixed', 'confirmed' ], + id => $id, + }; + $c->forward( 'output_requests', [ $criteria ] ); +} + +sub format_output : Private { + my ( $self, $c, $hashref ) = @_; + my $format = $c->stash->{format}; + if ('json' eq $format) { + $c->res->content_type('application/json; charset=utf-8'); + $c->res->body( encode_json($hashref) ); + } elsif ('xml' eq $format) { + $c->res->content_type('application/xml; charset=utf-8'); + $c->res->body( XMLout($hashref, RootName => undef) ); + } else { + $c->detach( 'error', [ + sprintf(_('Invalid format %s specified.'), $format) + ] ); + } +} + +sub is_jurisdiction_id_ok : Private { + my ( $self, $c ) = @_; + unless (my $jurisdiction_id = $c->req->param('jurisdiction_id')) { + $c->detach( 'error', [ _('Missing jurisdiction_id') ] ); + } +} + +# Input: DateTime object +# Output: 2011-04-23T10:28:55+02:00 +# FIXME Need generic solution to find time zone +sub w3date : Private { + my $datestr = shift; + return unless $datestr; + return DateTime::Format::W3CDTF->format_datetime($datestr); +} + +=head1 AUTHOR + +Copyright (c) 2011 Petter Reinholdtsen, some rights reserved. +Email: pere@hungry.com + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the GPL v2 or later. + +=cut + +__PACKAGE__->meta->make_immutable; + +1; |