aboutsummaryrefslogtreecommitdiffstats
path: root/perllib
diff options
context:
space:
mode:
Diffstat (limited to 'perllib')
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm32
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm32
-rw-r--r--perllib/FixMyStreet/DB/Result/Contact.pm25
-rw-r--r--perllib/FixMyStreet/DB/Result/Open311conf.pm39
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm108
-rw-r--r--perllib/Open311.pm198
-rw-r--r--perllib/Open311/GetUpdates.pm83
7 files changed, 511 insertions, 6 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index e5c0133cf..f7942c45a 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -325,6 +325,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');
+ }
}
}
@@ -342,6 +368,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/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index ffbb5a161..3c63d5c07 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
@@ -476,6 +477,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 +524,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 +544,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 +724,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.
diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm
index 001fb4ac6..779ca9bc2 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"]);
+__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.07010 @ 2011-06-23 15:49:48
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BXGd4uk1ybC5RTKlInTr0w
+# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-08-01 10:07:59
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4y6yRz4rMN66pBpkzfJJhg
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/Open311.pm b/perllib/Open311.pm
new file mode 100644
index 000000000..8c26a5cfc
--- /dev/null
+++ b/perllib/Open311.pm
@@ -0,0 +1,198 @@
+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' );
+
+sub get_service_list {
+ my $self = shift;
+
+ my $service_list_xml = $self->_get( 'services.xml' );
+
+ 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( 'requests.xml', $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 };
+ return $self->get_service_request_id_from_token( $token );
+ }
+ }
+ }
+}
+
+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( 'requests.xml', $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..5fecaf89c
--- /dev/null
+++ b/perllib/Open311/GetUpdates.pm
@@ -0,0 +1,83 @@
+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) {
+ warn 'updating problem ' . $p->id;
+ $p->update_from_open311_service_request( $request, $council_details, $self->system_user );
+ }
+ }
+
+ return 1;
+}
+
+1;