aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/open311-populate-service-list112
-rw-r--r--bin/open311-update-reports21
-rwxr-xr-xbin/send-reports29
-rw-r--r--db/schema.sql15
-rw-r--r--db/schema_0009-add_extra_to_problem.sql6
-rw-r--r--db/schema_0010-add_open311_conf.sql11
-rw-r--r--db/schema_0011-add_extra_to_contacts.sql6
-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
-rw-r--r--t/app/controller/admin.t53
-rw-r--r--t/app/controller/report_new_open311.t167
-rw-r--r--t/app/model/problem.t109
-rw-r--r--t/open311.t24
-rw-r--r--t/open311/getupdates.t198
-rw-r--r--templates/web/default/admin/council_contacts.html34
-rw-r--r--templates/web/default/common_header_tags.html6
-rw-r--r--templates/web/default/report/new/fill_in_details_form.html29
-rw-r--r--templates/web/default/report/new/fill_in_details_text.html3
-rw-r--r--web/css/core.css279
-rw-r--r--web/css/core.scss8
-rw-r--r--web/js/fixmystreet.js26
26 files changed, 1470 insertions, 183 deletions
diff --git a/bin/open311-populate-service-list b/bin/open311-populate-service-list
new file mode 100755
index 000000000..2a6733ef1
--- /dev/null
+++ b/bin/open311-populate-service-list
@@ -0,0 +1,112 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use LWP::Simple;
+use XML::Simple;
+use FixMyStreet::App;
+use Open311;
+
+use Data::Dumper;
+
+my $council_list = FixMyStreet::App->model('DB::Open311conf');
+
+while ( my $council = $council_list->next ) {
+
+ my $open311 = Open311->new(
+ endpoint => $council->endpoint,
+ jurisdiction => $council->jurisdiction,
+ api_key => $council->api_key
+ );
+
+ my $list = $open311->get_service_list;
+
+ my @found_contacts;
+
+ # print Dumper $list;
+
+ foreach my $service ( @{ $list->{service} } ) {
+ print $service->{service_code} . ': ' . $service->{service_name} . "\n";
+ my $contacts = FixMyStreet::App->model( 'DB::Contact')->search(
+ {
+ area_id => $council->area_id,
+ -OR => [
+ email => $service->{service_code},
+ category => $service->{service_name}
+ ]
+ }
+ );
+
+ my $contact = $contacts->first;
+
+ # FIXME - handle change of service name or service code
+ if ( $contact ) {
+
+ print $council->area_id . " already has a contact for service code " . $service->{service_code} . "\n";
+ push @found_contacts, $service->{service_code};
+
+ if ( $contact->deleted ) {
+ $contact->update(
+ {
+ category => $service->{service_name},
+ email => $service->{service_code},
+ confirmed => 1,
+ deleted => 0,
+ editor => $0,
+ whenedited => \'ms_current_timestamp()',
+ note => 'automatically undeleted by script',
+ }
+ );
+ }
+ } else {
+ my $contact = FixMyStreet::App->model( 'DB::Contact')->create(
+ {
+ email => $service->{service_code},
+ area_id => $council->area_id,
+ category => $service->{service_name},
+ confirmed => 1,
+ deleted => 0,
+ editor => $0,
+ whenedited => \'ms_current_timestamp()',
+ note => 'created automatically by script',
+ }
+ );
+
+ if ( lc( $service->{metadata} ) eq 'true' ) {
+ print "Fetching meta data for $service->{service_code}\n";
+ my $meta_data = $open311->get_service_meta_info( $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;
+ }
+
+ push @found_contacts, $service->{service_code};
+ print "created contact for service code " . $service->{service_code} . " for council @{[$council->area_id]}\n";
+ }
+ }
+
+ my $found_contacts = FixMyStreet::App->model( 'DB::Contact')->search(
+ {
+ email => { -not_in => \@found_contacts },
+ area_id => $council->area_id,
+ deleted => 0,
+ }
+ );
+
+ $found_contacts->update(
+ {
+ deleted => 1,
+ editor => $0,
+ whenedited => \'ms_current_timestamp()',
+ note => 'automatically marked as deleted by script'
+ }
+ );
+}
diff --git a/bin/open311-update-reports b/bin/open311-update-reports
new file mode 100644
index 000000000..41c9c4546
--- /dev/null
+++ b/bin/open311-update-reports
@@ -0,0 +1,21 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Open311::GetUpdates;
+use FixMyStreet::App;
+
+# FIXME - make this configurable and/or better
+my $system_user = FixMyStreet::App->model('DB::User')->find_or_create(
+ {
+ email => FixMyStreet::App->config->{'CONTACT_EMAIL'},
+ name => 'System User',
+ }
+);
+
+my $council_list = FixMyStreet::App->model('DB::Open311conf');
+
+my $update = Open311::GetUpdates->new(
+ council_list => $council_list,
+ system_user => $system_user
+)->get_updates;
diff --git a/bin/send-reports b/bin/send-reports
index 1af3ba1ea..298eb458d 100755
--- a/bin/send-reports
+++ b/bin/send-reports
@@ -28,6 +28,8 @@ use mySociety::EmailUtil;
use mySociety::MaPit;
use mySociety::Web qw(ent);
+use Open311;
+
# Set up site, language etc.
my ($verbose, $nomail) = CronFns::options();
my $base_url = mySociety::Config::get('BASE_URL');
@@ -136,6 +138,8 @@ while (my $row = $unsent->next) {
$h{category} = 'Customer Services' if $h{category} eq 'Other';
} elsif ($areas_info->{$council}->{type} eq 'LBO') { # London
$send_web = 'london';
+ } elsif ( FixMyStreet::App->model("DB::Open311conf")->find( { area_id => $council } ) ) {
+ $send_web = 'open311';
} else {
my $contact = FixMyStreet::App->model("DB::Contact")->find( {
deleted => 0,
@@ -248,6 +252,31 @@ while (my $row = $unsent->next) {
if (!$nomail) {
$result *= post_london_report( $row, %h );
}
+ } elsif ($send_web eq 'open311') {
+ # FIXME - looking this up twice :(
+ my $conf = FixMyStreet::App->model('DB::Open311conf')->find( { area_id => $row->council } );
+
+ # FIXME - doesn't deal with multiple recipients
+ my $contact = FixMyStreet::App->model("DB::Contact")->find( {
+ deleted => 0,
+ area_id => $row->council,
+ category => $row->category
+ } );
+
+ my $open311 = Open311->new(
+ jurisdiction => $conf->jurisdiction,
+ endpoint => $conf->endpoint,
+ api_key => $conf->api_key,
+ );
+
+ my $resp = $open311->send_service_request( $row, \%h, $contact->email );
+
+ if ( $resp ) {
+ $row->external_id( $resp );
+ $result = 0;
+ } else {
+ $result = 1;
+ }
}
if ($result == mySociety::EmailUtil::EMAIL_SUCCESS) {
diff --git a/db/schema.sql b/db/schema.sql
index 99cf2832d..fcd137919 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -78,7 +78,10 @@ create table contacts (
-- time of last change
whenedited timestamp not null,
-- what the last change was for: author's notes
- note text not null
+ note text not null,
+
+ -- extra fields required for open311
+ extra text
);
create unique index contacts_area_id_category_idx on contacts(area_id, category);
@@ -184,6 +187,7 @@ create table problem (
lastupdate timestamp not null default ms_current_timestamp(),
whensent timestamp,
send_questionnaire boolean not null default 't',
+ extra text, -- extra fields required for open311
flagged boolean not null default 'f'
);
create index problem_state_latitude_longitude_idx on problem(state, latitude, longitude);
@@ -407,3 +411,12 @@ create table admin_log (
whenedited timestamp not null default ms_current_timestamp()
);
+-- Record open 311 configuration details
+
+create table open311conf (
+ id serial primary key,
+ area_id integer not null unique,
+ endpoint text not null,
+ jurisdiction text,
+ api_key text
+);
diff --git a/db/schema_0009-add_extra_to_problem.sql b/db/schema_0009-add_extra_to_problem.sql
new file mode 100644
index 000000000..bac5806c7
--- /dev/null
+++ b/db/schema_0009-add_extra_to_problem.sql
@@ -0,0 +1,6 @@
+begin;
+
+ALTER TABLE problem
+ ADD COLUMN extra TEXT;
+
+commit;
diff --git a/db/schema_0010-add_open311_conf.sql b/db/schema_0010-add_open311_conf.sql
new file mode 100644
index 000000000..920272c05
--- /dev/null
+++ b/db/schema_0010-add_open311_conf.sql
@@ -0,0 +1,11 @@
+begin;
+
+CREATE TABLE open311conf (
+ id SERIAL PRIMARY KEY,
+ area_id INTEGER NOT NULL unique,
+ endpoint TEXT NOT NULL,
+ jurisdiction TEXT,
+ api_key TEXT
+);
+
+commit;
diff --git a/db/schema_0011-add_extra_to_contacts.sql b/db/schema_0011-add_extra_to_contacts.sql
new file mode 100644
index 000000000..fd6eae807
--- /dev/null
+++ b/db/schema_0011-add_extra_to_contacts.sql
@@ -0,0 +1,6 @@
+begin;
+
+ALTER TABLE contacts
+ ADD COLUMN extra TEXT;
+
+commit;
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;
diff --git a/t/app/controller/admin.t b/t/app/controller/admin.t
index 08cb4fb0d..be3b74cf5 100644
--- a/t/app/controller/admin.t
+++ b/t/app/controller/admin.t
@@ -205,6 +205,59 @@ subtest 'check contact updating' => sub {
$mech->content_like(qr{test2\@example.com[^<]*</td>[^<]*<td><strong>Yes}s);
};
+my $open311 =
+ FixMyStreet::App->model('DB::Open311Conf')->search( { area_id => 2650 } );
+$open311->delete if $open311;
+
+subtest 'check open311 configuring' => sub {
+ $mech->get_ok('/admin/council_contacts/2650/');
+ $mech->content_lacks('Council contacts configured via Open311');
+
+ $mech->form_number(3);
+ $mech->submit_form_ok(
+ {
+ with_fields => {
+ api_key => 'api key',
+ endpoint => 'http://example.com/open311',
+ jurisdiction => 'mySociety',
+ }
+ }
+ );
+ $mech->content_contains('Council contacts configured via Open311');
+ $mech->content_contains('Configuration updated - contacts will be generated automatically later');
+
+ $open311 =
+ FixMyStreet::App->model('DB::Open311Conf')->search( { area_id => 2650 } );
+
+ is $open311->count, 1, 'only one configuration';
+ my $conf = $open311->first;
+ is $conf->endpoint, 'http://example.com/open311', 'endpoint configured';
+ is $conf->api_key, 'api key', 'api key configured';
+ is $conf->jurisdiction, 'mySociety', 'jurisdiction configures';
+
+ $mech->form_number(3);
+ $mech->submit_form_ok(
+ {
+ with_fields => {
+ api_key => 'new api key',
+ endpoint => 'http://example.org/open311',
+ jurisdiction => 'open311',
+ }
+ }
+ );
+
+ $mech->content_contains('Configuration updated');
+
+ $open311 =
+ FixMyStreet::App->model('DB::Open311Conf')->search( { area_id => 2650 } );
+
+ is $open311->count, 1, 'only one configuration';
+ $conf = $open311->first;
+ is $conf->endpoint, 'http://example.org/open311', 'endpoint updated';
+ is $conf->api_key, 'new api key', 'api key updated';
+ is $conf->jurisdiction, 'open311', 'jurisdiction configures';
+};
+
subtest 'check text output' => sub {
$mech->get_ok('/admin/council_contacts/2650?text=1');
is $mech->content_type, 'text/plain';
diff --git a/t/app/controller/report_new_open311.t b/t/app/controller/report_new_open311.t
new file mode 100644
index 000000000..dc3583e6b
--- /dev/null
+++ b/t/app/controller/report_new_open311.t
@@ -0,0 +1,167 @@
+use strict;
+use warnings;
+use Test::More;
+
+use FixMyStreet::TestMech;
+use Web::Scraper;
+
+my $mech = FixMyStreet::TestMech->new;
+
+my $open311Conf = FixMyStreet::App->model('DB::Open311Conf')->find_or_create( {
+ area_id => 2651,
+ endpoint => 'http://example.com/open311',
+ jurisdiction => 'mySociety',
+ api_key => 'apikey',
+} );
+
+my %contact_params = (
+ confirmed => 1,
+ deleted => 0,
+ editor => 'Test',
+ whenedited => \'current_timestamp',
+ note => 'Created for test',
+);
+# Let's make some contacts to send things to!
+my $contact1 = FixMyStreet::App->model('DB::Contact')->find_or_create( {
+ %contact_params,
+ area_id => 2651, # Edinburgh
+ category => 'Street lighting',
+ email => '100',
+ extra => [ { description => 'Lamppost number', code => 'number', required => 'True' },
+ { description => 'Lamppost type', code => 'type', required => 'False', values =>
+ { value => { Yellow => { key => 'modern' }, 'Gas' => { key => 'old' } } }
+ }
+ ],
+} );
+my $contact2 = FixMyStreet::App->model('DB::Contact')->find_or_create( {
+ %contact_params,
+ area_id => 2651, # Edinburgh
+ category => 'Graffiti Removal',
+ email => '101',
+} );
+ok $contact1, "created test contact 1";
+ok $contact2, "created test contact 2";
+
+# test that the various bit of form get filled in and errors correctly
+# generated.
+foreach my $test (
+ {
+ msg => 'all fields empty',
+ pc => 'EH99 1SP',
+ fields => {
+ title => '',
+ detail => '',
+ photo => '',
+ name => '',
+ may_show_name => '1',
+ email => '',
+ phone => '',
+ category => 'Street lighting',
+ password_sign_in => '',
+ password_register => '',
+ remember_me => undef,
+ },
+ changes => {
+ number => '',
+ type => 'old',
+ },
+ errors => [
+ 'Please enter a subject',
+ 'Please enter some details',
+ 'This information is required',
+ 'Please enter your email',
+ 'Please enter your name',
+ ],
+ submit_with => {
+ title => 'test',
+ detail => 'test detail',
+ name => 'Test User',
+ email => 'testopen311@example.com',
+ category => 'Street lighting',
+ number => 27,
+ },
+ extra => [
+ {
+ name => 'number',
+ value => 27,
+ description => 'Lamppost number',
+ },
+ {
+ name => 'type',
+ value => 'old',
+ description => 'Lamppost type',
+ }
+ ]
+ },
+ )
+{
+ subtest "check form errors where $test->{msg}" => sub {
+ $mech->log_out_ok;
+ $mech->clear_emails_ok;
+
+ # check that the user does not exist
+ my $test_email = $test->{submit_with}->{email};
+ my $user = FixMyStreet::App->model('DB::User')->find( { email => $test_email } );
+ if ( $user ) {
+ $user->problems->delete;
+ $user->comments->delete;
+ $user->delete;
+ }
+
+ $mech->get_ok('/around');
+
+ # submit initial pc form
+ $mech->submit_form_ok( { with_fields => { pc => $test->{pc} } },
+ "submit location" );
+ is_deeply $mech->form_errors, [], "no errors for pc '$test->{pc}'";
+
+ # click through to the report page
+ $mech->follow_link_ok( { text => 'skip this step', },
+ "follow 'skip this step' link" );
+
+ # submit the main form
+ $mech->submit_form_ok( { with_fields => $test->{fields} },
+ "submit form" );
+
+ # check that we got the errors expected
+ is_deeply $mech->form_errors, $test->{errors}, "check errors";
+
+ # check that fields have changed as expected
+ my $new_values = {
+ %{ $test->{fields} }, # values added to form
+ %{ $test->{changes} }, # changes we expect
+ };
+ is_deeply $mech->visible_form_values, $new_values,
+ "values correctly changed";
+
+ if ( $test->{fields}->{category} eq 'Street lighting' ) {
+ my $result = scraper {
+ process 'div#category_meta div select#form_type option', 'option[]' => '@value';
+ }
+ ->scrape( $mech->response );
+
+ is_deeply $result->{option}, [ qw/old modern/], 'displayed streetlight type select';
+ }
+
+ $new_values = {
+ %{ $test->{fields} },
+ %{ $test->{submit_with} },
+ };
+ $mech->submit_form_ok( { with_fields => $new_values } );
+
+ $user = FixMyStreet::App->model('DB::User')->find( { email => $test_email } );
+ ok $user, 'created user';
+ my $prob = $user->problems->first;
+ ok $prob, 'problem created';
+
+ is_deeply $prob->extra, $test->{extra}, 'extra open311 data added to problem';
+
+ $user->problems->delete;
+ $user->delete;
+ };
+}
+
+$contact1->delete;
+$contact2->delete;
+
+done_testing();
diff --git a/t/app/model/problem.t b/t/app/model/problem.t
index 4c6be6a8d..ad42c5fdf 100644
--- a/t/app/model/problem.t
+++ b/t/app/model/problem.t
@@ -152,6 +152,111 @@ for my $test (
};
}
+my $user = FixMyStreet::App->model('DB::User')->find_or_create(
+ {
+ email => 'system_user@example.com'
+ }
+);
+
+$problem->user( $user );
+$problem->created( DateTime->now()->subtract( days => 1 ) );
+$problem->lastupdate( DateTime->now()->subtract( days => 1 ) );
+$problem->anonymous(1);
+$problem->insert;
+
+my $tz_local = DateTime::TimeZone->new( name => 'local' );
+
+for my $test (
+ {
+ desc => 'request older than problem ignored',
+ lastupdate => '',
+ request => {
+ updated_datetime => DateTime::Format::W3CDTF->new()->format_datetime( DateTime->now()->set_time_zone( $tz_local )->subtract( days => 2 ) ),
+ },
+ council => {
+ name => 'Edinburgh City Council',
+ },
+ created => 0,
+ },
+ {
+ desc => 'request newer than problem created',
+ lastupdate => '',
+ request => {
+ updated_datetime => DateTime::Format::W3CDTF->new()->format_datetime( DateTime->now()->set_time_zone( $tz_local ) ),
+ status => 'open',
+ status_notes => 'this is an update from the council',
+ },
+ council => {
+ name => 'Edinburgh City Council',
+ },
+ created => 1,
+ state => 'confirmed',
+ mark_fixed => 0,
+ mark_open => 0,
+ },
+ {
+ desc => 'update with state of closed fixes problem',
+ lastupdate => '',
+ request => {
+ updated_datetime => DateTime::Format::W3CDTF->new()->format_datetime( DateTime->now()->set_time_zone( $tz_local ) ),
+ status => 'closed',
+ status_notes => 'the council have fixed this',
+ },
+ council => {
+ name => 'Edinburgh City Council',
+ },
+ created => 1,
+ state => 'fixed',
+ mark_fixed => 1,
+ mark_open => 0,
+ },
+ {
+ desc => 'update with state of open leaves problem as fixed',
+ lastupdate => '',
+ request => {
+ updated_datetime => DateTime::Format::W3CDTF->new()->format_datetime( DateTime->now()->set_time_zone( $tz_local ) ),
+ status => 'open',
+ status_notes => 'the council do not think this is fixed',
+ },
+ council => {
+ name => 'Edinburgh City Council',
+ },
+ created => 1,
+ start_state => 'fixed',
+ state => 'fixed',
+ mark_fixed => 0,
+ mark_open => 0,
+ },
+) {
+ subtest $test->{desc} => sub {
+ # makes testing easier;
+ $problem->comments->delete;
+ $problem->created( DateTime->now()->subtract( days => 1 ) );
+ $problem->lastupdate( DateTime->now()->subtract( days => 1 ) );
+ $problem->state( $test->{start_state} || 'confirmed' );
+ $problem->update;
+ my $w3c = DateTime::Format::W3CDTF->new();
+
+ my $ret = $problem->update_from_open311_service_request( $test->{request}, $test->{council}, $user );
+ is $ret, $test->{created}, 'return value';
+
+ return unless $test->{created};
+
+ $problem->discard_changes;
+ is $problem->lastupdate, $w3c->parse_datetime($test->{request}->{updated_datetime}), 'lastupdate time';
+
+ my $update = $problem->comments->first;
+
+ ok $update, 'updated created';
+
+ is $problem->state, $test->{state}, 'problem state';
+
+ is $update->text, $test->{request}->{status_notes}, 'update text';
+ is $update->mark_open, $test->{mark_open}, 'update mark_open flag';
+ is $update->mark_fixed, $test->{mark_fixed}, 'update mark_fixed flag';
+ };
+}
+
for my $test (
{
state => 'partial',
@@ -240,4 +345,8 @@ for my $test (
};
}
+$problem->comments->delete;
+$problem->delete;
+$user->delete;
+
done_testing();
diff --git a/t/open311.t b/t/open311.t
new file mode 100644
index 000000000..f7a8cd815
--- /dev/null
+++ b/t/open311.t
@@ -0,0 +1,24 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More tests => 4;
+
+use FindBin;
+use lib "$FindBin::Bin/../perllib";
+use lib "$FindBin::Bin/../commonlib/perllib";
+
+use_ok( 'Open311' );
+
+my $o = Open311->new();
+ok $o, 'created object';
+
+my $err_text = <<EOT
+<?xml version="1.0" encoding="utf-8"?><errors><error><code>400</code><description>Service Code cannot be null -- can't proceed with the request.</description></error></errors>
+EOT
+;
+
+is $o->_process_error( $err_text ), "400: Service Code cannot be null -- can't proceed with the request.\n", 'error text parsing';
+is $o->_process_error( '503 - service unavailable' ), 'unknown error', 'error text parsing of bad error';
+
+
diff --git a/t/open311/getupdates.t b/t/open311/getupdates.t
new file mode 100644
index 000000000..03af0b72b
--- /dev/null
+++ b/t/open311/getupdates.t
@@ -0,0 +1,198 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More;
+
+use FindBin;
+use lib "$FindBin::Bin/../perllib";
+use lib "$FindBin::Bin/../commonlib/perllib";
+
+use_ok( 'Open311::GetUpdates' );
+use_ok( 'Open311' );
+
+my $user = FixMyStreet::App->model('DB::User')->find_or_create(
+ {
+ email => 'system_user@example.com'
+ }
+);
+
+
+my $updates = Open311::GetUpdates->new( system_user => $user );
+ok $updates, 'created object';
+
+my $requests_xml = qq{<?xml version="1.0" encoding="utf-8"?>
+<service_requests>
+<request>
+<service_request_id>638344</service_request_id>
+<status>open</status>
+<status_notes>This is a note.</status_notes>
+<service_name>Sidewalk and Curb Issues</service_name>
+<service_code>006</service_code>
+<description></description>
+<agency_responsible></agency_responsible>
+<service_notice></service_notice>
+<requested_datetime>2010-04-14T06:37:38-08:00</requested_datetime>
+UPDATED_DATETIME
+<expected_datetime>2010-04-15T06:37:38-08:00</expected_datetime>
+<lat>37.762221815</lat>
+<long>-122.4651145</long>
+</request>
+</service_requests>
+};
+
+my $problem_rs = FixMyStreet::App->model('DB::Problem');
+my $problem = $problem_rs->new(
+ {
+ postcode => 'EH99 1SP',
+ latitude => 1,
+ longitude => 1,
+ areas => 1,
+ title => '',
+ detail => '',
+ used_map => 1,
+ user_id => 1,
+ name => '',
+ state => 'confirmed',
+ service => '',
+ cobrand => 'default',
+ cobrand_data => '',
+ user => $user,
+ created => DateTime->now()->subtract( days => 1 ),
+ lastupdate => DateTime->now()->subtract( days => 1 ),
+ anonymous => 1,
+ external_id => 638344,
+ }
+);
+
+$problem->insert;
+
+for my $test (
+ {
+ desc => 'element missing',
+ updated_datetime => '',
+ comment_count => 0,
+ },
+ {
+ desc => 'empty element',
+ updated_datetime => '<updated_datetime />',
+ comment_count => 0,
+ },
+ {
+ desc => 'element with no content',
+ updated_datetime => '<updated_datetime></updated_datetime>',
+ comment_count => 0,
+ },
+ {
+ desc => 'element with old content',
+ updated_datetime => sprintf( '<updated_datetime>%s</updated_datetime>', DateTime->now->subtract( days => 3 ) ),
+ comment_count => 0,
+ },
+ {
+ desc => 'element with new content',
+ updated_datetime => sprintf( '<updated_datetime>%s</updated_datetime>', DateTime->now ),
+ comment_count => 1,
+ },
+) {
+ subtest $test->{desc} => sub {
+ $problem->comments->delete;
+ $problem->lastupdate(DateTime->now()->subtract( days => 1 ) ),
+ $problem->update;
+
+ my $local_requests_xml = $requests_xml;
+ $local_requests_xml =~ s/UPDATED_DATETIME/$test->{updated_datetime}/;
+
+ my $o = Open311->new( jurisdiction => 'mysociety', endpoint => 'http://example.com', test_mode => 1, test_get_returns => { 'requests.xml' => $local_requests_xml } );
+
+ ok $updates->update_reports( [ 638344 ], $o, { name => 'Test Council' } );
+ is $o->test_uri_used, 'http://example.com/requests.xml?jurisdiction_id=mysociety&service_request_id=638344', 'get url';
+
+ is $problem->comments->count, $test->{comment_count}, 'added a comment';
+ };
+}
+
+$requests_xml = qq{<?xml version="1.0" encoding="utf-8"?>
+<service_requests>
+<request>
+<service_request_id>638344</service_request_id>
+<status>open</status>
+<status_notes>This is a note.</status_notes>
+<service_name>Sidewalk and Curb Issues</service_name>
+<service_code>006</service_code>
+<description></description>
+<agency_responsible></agency_responsible>
+<service_notice></service_notice>
+<requested_datetime>2010-04-14T06:37:38-08:00</requested_datetime>
+<updated_datetime>UPDATED_DATETIME</updated_datetime>
+<expected_datetime>2010-04-15T06:37:38-08:00</expected_datetime>
+<lat>37.762221815</lat>
+<long>-122.4651145</long>
+</request>
+<request>
+<service_request_id>638345</service_request_id>
+<status>open</status>
+<status_notes>This is a for a different issue.</status_notes>
+<service_name>Sidewalk and Curb Issues</service_name>
+<service_code>006</service_code>
+<description></description>
+<agency_responsible></agency_responsible>
+<service_notice></service_notice>
+<requested_datetime>2010-04-14T06:37:38-08:00</requested_datetime>
+<updated_datetime>UPDATED_DATETIME2</updated_datetime>
+<expected_datetime>2010-04-15T06:37:38-08:00</expected_datetime>
+<lat>37.762221815</lat>
+<long>-122.4651145</long>
+</request>
+</service_requests>
+};
+
+my $problem2 = $problem_rs->new(
+ {
+ postcode => 'EH99 1SP',
+ latitude => 1,
+ longitude => 1,
+ areas => 1,
+ title => '',
+ detail => '',
+ used_map => 1,
+ user_id => 1,
+ name => '',
+ state => 'confirmed',
+ service => '',
+ cobrand => 'default',
+ cobrand_data => '',
+ user => $user,
+ created => DateTime->now()->subtract( days => 1 ),
+ lastupdate => DateTime->now()->subtract( days => 1 ),
+ anonymous => 1,
+ external_id => 638345,
+ }
+);
+
+$problem->comments->delete;
+subtest 'update with two requests' => sub {
+ $problem->comments->delete;
+ $problem->lastupdate(DateTime->now()->subtract( days => 1 ) ),
+
+ my $date1 = DateTime::Format::W3CDTF->new->formate_datetime( DateTime->now() );
+ my $date2 = DateTime::Format::W3CDTF->new->formate_datetime( DateTime->now->subtract( hour => 1) );
+ my $local_requests_xml = $requests_xml;
+ $local_requests_xml =~ s/UPDATED_DATETIME2/$date2/;
+ $local_requests_xml =~ s/UPDATED_DATETIME/$date1/;
+
+ my $o = Open311->new( jurisdiction => 'mysociety', endpoint => 'http://example.com', test_mode => 1, test_get_returns => { 'requests.xml' => $local_requests_xml } );
+
+ ok $updates->update_reports( [ 638344,638345 ], $o, { name => 'Test Council' } );
+ is $o->test_uri_used, 'http://example.com/requests.xml?jurisdiction_id=mysociety&service_request_id=638344,638345', 'get url';
+
+ is $problem->comments->count, 1, 'added a comment to first problem';
+ is $problem2->comments->count, 1, 'added a comment to second problem';
+};
+
+$problem->comments->delete;
+$problem->delete;
+$user->comments->delete;
+$user->problems->delete;
+$user->delete;
+
+done_testing();
diff --git a/templates/web/default/admin/council_contacts.html b/templates/web/default/admin/council_contacts.html
index 669f137f9..75d915a29 100644
--- a/templates/web/default/admin/council_contacts.html
+++ b/templates/web/default/admin/council_contacts.html
@@ -12,6 +12,13 @@
<a href="[% c.uri_for( 'council_contacts', area_id, { text => 1 } ) %]">[% loc('Text only version') %]</a>
</p>
+[% IF open311.count > 0 %]
+ <h2>
+ Council contacts configured via Open311
+ </h2>
+
+[% END %]
+
<form method="post" action="[% c.uri_for('council_contacts', area_id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<table cellspacing="0" cellpadding="2" border="1">
@@ -86,4 +93,31 @@
</div>
</form>
+ <h2>[% loc('Configure Open311 integration') %]</h2>
+ <form method="post" action="[% c.uri_for('council_contacts', area_id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+ [% conf = open311.next %]
+ <p>
+ <label for="endpoint">Endpoint</label>:
+ <input type="text" name="endpoint" id="endpoint" value="[% conf.endpoint %]" size="50">
+ </p>
+
+ <p>
+ <label for="jurisdiction">Jurisdiction</label>:
+ <input type="text" name="jurisdiction" id="jurisdiction" value="[% conf.jurisdiction %]" size="50">
+ </p>
+
+ <p>
+ <label for="api_key">Api Key</label>:
+ <input type="text" name="api_key" id="api_key" value="[% conf.api_key %]" size="25">
+ </p>
+
+ <p>
+ <input type="hidden" name="open311_id" value="[% conf.id %]">
+ <input type="hidden" name="area_id" value="[% area_id %]">
+ <input type="hidden" name="posted" value="open311">
+ <input type="hidden" name="token" value="[% token %]">
+ <input type="submit" name="Configure Open311" value="[% loc('Configure Open311') %]">
+ </p>
+ </form>
+
[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/default/common_header_tags.html b/templates/web/default/common_header_tags.html
index f9048b067..206a0e6d4 100644
--- a/templates/web/default/common_header_tags.html
+++ b/templates/web/default/common_header_tags.html
@@ -4,6 +4,12 @@
[% map_js %]
+[% IF category_extras_json && category_extras_json != '{}' %]
+<script type="text/javascript">
+ category_extras = [% category_extras_json %];
+</script>
+[% END %]
+
[% IF robots %]
<meta name="robots" content="[% robots %]">
[% ELSIF c.config.STAGING_SITE %]
diff --git a/templates/web/default/report/new/fill_in_details_form.html b/templates/web/default/report/new/fill_in_details_form.html
index eea020e3f..a0c50d5af 100644
--- a/templates/web/default/report/new/fill_in_details_form.html
+++ b/templates/web/default/report/new/fill_in_details_form.html
@@ -79,6 +79,35 @@
[% END %]
[% END %]
+[%- IF category_extras %]
+<div id="category_meta">
+ [%- IF report_meta %]
+ [%- category = report.category %]
+ <h4>Additional Information</h4>
+ [%- FOR meta IN category_extras.$category %]
+ [%- meta_name = meta.code -%]
+
+[% IF field_errors.$meta_name %]
+ <div class='form-error'>[% field_errors.$meta_name %]</div>
+[% END -%]
+
+ <div class="form-field">
+ <label for="form_[% meta_name %]">[% meta.description _ ':' %]</label>
+ [% IF meta.exists('values') %]
+ <select name="[% meta_name %]" id="form_[% meta_name %]">
+ [% FOR option IN meta.values.value.keys %]
+ <option value="[% meta.values.value.$option.key %]">[% option %]</option>
+ [% END %]
+ </select>
+ [% ELSE %]
+ <input type="text" value="[% report_meta.$meta_name | html %]" name="[% meta_name %]" id="form_[% meta_name %]">
+ [% END %]
+ </div>
+ [%- END %]
+ [%- END %]
+</div>
+[%- END %]
+
[% IF c.cobrand.allow_photo_upload %]
[% IF field_errors.photo %]
<div class='form-error'>[% field_errors.photo %]</div>
diff --git a/templates/web/default/report/new/fill_in_details_text.html b/templates/web/default/report/new/fill_in_details_text.html
index 9ebb8107b..5d9716915 100644
--- a/templates/web/default/report/new/fill_in_details_text.html
+++ b/templates/web/default/report/new/fill_in_details_text.html
@@ -4,6 +4,9 @@
to help unless you leave as much detail as you can, so please describe the exact location of
the problem (e.g. on a wall), what it is, how long it has been there, a description (and a
photo of the problem if you have one), etc.');
+ IF category_extras;
+ ' ' _ loc('Some categories may require additional information.');
+ END;
ELSE;
loc('Please fill in details of the problem below.');
END;
diff --git a/web/css/core.css b/web/css/core.css
index 309d8a9cb..88181e3e8 100644
--- a/web/css/core.css
+++ b/web/css/core.css
@@ -1,60 +1,48 @@
#mysociety blockquote {
border-left: solid 4px #666666;
- padding-left: 0.5em;
-}
-#mysociety blockquote h2, #mysociety blockquote p {
- margin: 0;
-}
+ padding-left: 0.5em; }
+ #mysociety blockquote h2, #mysociety blockquote p {
+ margin: 0; }
#mysociety dt {
font-weight: bold;
- margin-top: 0.5em;
-}
+ margin-top: 0.5em; }
#mysociety .gone {
color: #666666;
- background-color: #cccccc;
-}
+ background-color: #cccccc; }
#mysociety p.dev-site-notice, #mysociety p.error {
text-align: center;
color: #cc0000;
- font-size: larger;
-}
+ font-size: larger; }
#mysociety ul {
padding: 0 0 0 1.5em;
- margin: 0;
-}
+ margin: 0; }
#mysociety ul.error {
color: #cc0000;
background-color: #ffeeee;
padding-right: 4px;
text-align: left;
- font-size: larger;
-}
+ font-size: larger; }
#mysociety div.form-error {
color: #cc0000;
margin: 5px 1em 5px 1em;
padding: 2px 5px 2px 5px;
float: left;
background-color: #ffeeee;
- text-align: left;
-}
+ text-align: left; }
#mysociety div.form-field {
- clear: both;
-}
+ clear: both; }
#mysociety #advert_thin {
width: 50%;
margin: 1em auto;
text-align: center;
- border-top: dotted 1px #999999;
-}
+ border-top: dotted 1px #999999; }
#mysociety #advert_hfymp {
border-top: dotted 1px #999999;
- text-align: center;
-}
+ text-align: center; }
#mysociety p#expl {
text-align: center;
font-size: 150%;
- margin: 0 2em;
-}
+ margin: 0 2em; }
#mysociety #postcodeForm {
display: table;
_width: 33em;
@@ -64,23 +52,18 @@
padding: 1em;
-moz-border-radius: 1em;
-webkit-border-radius: 1em;
- border-radius: 1em;
-}
-#mysociety #postcodeForm label {
- float: none;
- padding-right: 0;
-}
-#mysociety #postcodeForm #submit {
- font-size: 83%;
-}
+ border-radius: 1em; }
+ #mysociety #postcodeForm label {
+ float: none;
+ padding-right: 0; }
+ #mysociety #postcodeForm #submit {
+ font-size: 83%; }
#mysociety #front_intro {
float: left;
- width: 48%;
-}
-#mysociety #front_intro p {
- clear: both;
- margin-top: 0;
-}
+ width: 48%; }
+ #mysociety #front_intro p {
+ clear: both;
+ margin-top: 0; }
#mysociety #front_stats div {
text-align: center;
width: 5.5em;
@@ -88,128 +71,106 @@
-webkit-border-radius: 0.5em;
border-radius: 0.5em;
float: left;
- margin: 0 1em 1em;
-}
-#mysociety #front_stats div big {
- font-size: 150%;
- display: block;
-}
+ margin: 0 1em 1em; }
+ #mysociety #front_stats div big {
+ font-size: 150%;
+ display: block; }
#mysociety #front_recent {
float: right;
width: 48%;
- margin-bottom: 1em;
-}
+ margin-bottom: 1em; }
#mysociety #front_recent img, #mysociety #alert_recent img {
margin-right: 0.25em;
- margin-bottom: 0.25em;
-}
+ margin-bottom: 0.25em; }
#mysociety #front_recent > h2:first-child, #mysociety #front_intro > h2:first-child {
- margin-top: 0;
-}
+ margin-top: 0; }
#mysociety form {
- margin: 0;
-}
+ margin: 0; }
#mysociety label {
float: left;
text-align: right;
padding-right: 0.5em;
- width: 5em;
-}
+ width: 5em; }
#mysociety fieldset, #mysociety #fieldset {
border: none;
- padding: 0.5em;
-}
-#mysociety fieldset div, #mysociety #fieldset div {
- margin-top: 2px;
- clear: left;
-}
+ padding: 0.5em; }
+ #mysociety fieldset div, #mysociety #fieldset div {
+ margin-top: 2px;
+ clear: left; }
#mysociety legend {
- display: none;
-}
+ display: none; }
#mysociety #fieldset div.checkbox, #mysociety #problem_submit {
- padding-left: 5.5em;
-}
+ padding-left: 5.5em; }
#mysociety #fieldset div.checkbox label, #mysociety label.n {
float: none;
text-align: left;
padding-right: 0;
width: auto;
cursor: pointer;
- cursor: hand;
-}
+ cursor: hand; }
#mysociety #questionnaire label, #mysociety #alerts label {
- float: none;
-}
+ float: none; }
#mysociety .confirmed {
background-color: #ccffcc;
border: solid 2px #009900;
padding: 5px;
- text-align: center;
-}
+ text-align: center; }
#mysociety #form_sign_in_yes {
float: left;
width: 47%;
padding-right: 1%;
border-right: solid 1px #999999;
- margin-bottom: 1em;
-}
+ margin-bottom: 1em; }
#mysociety #form_sign_in_no, #mysociety #fieldset #form_sign_in_no {
float: right;
width: 47%;
padding-left: 1%;
clear: none;
- margin-bottom: 1em;
-}
+ margin-bottom: 1em; }
+#mysociety #category_meta {
+ margin-bottom: 30px; }
+#mysociety #category_meta label {
+ width: 10em; }
#mysociety #map_box {
float: right;
width: 502px;
position: relative;
padding-left: 20px;
- background-color: #ffffff;
-}
+ background-color: #ffffff; }
#mysociety p#copyright {
float: right;
text-align: right;
margin: 0 0 1em 0;
- font-size: 78%;
-}
+ font-size: 78%; }
#mysociety #map {
border: solid 1px #000000;
width: 500px;
height: 500px;
overflow: hidden;
position: relative;
- background-color: #f1f1f1;
-}
+ background-color: #f1f1f1; }
#mysociety #drag {
position: absolute;
width: 500px;
height: 500px;
right: 0;
- top: 0;
-}
-#mysociety #drag input, #mysociety #drag img {
- position: absolute;
- border: none;
-}
-#mysociety #drag input {
- cursor: crosshair;
- background-color: #cccccc;
-}
-#mysociety #drag img {
- cursor: move;
-}
-#mysociety #drag img.pin {
- z-index: 100;
- background-color: inherit;
-}
-#mysociety #drag a img.pin {
- cursor: pointer;
- cursor: hand;
-}
+ top: 0; }
+ #mysociety #drag input, #mysociety #drag img {
+ position: absolute;
+ border: none; }
+ #mysociety #drag input {
+ cursor: crosshair;
+ background-color: #cccccc; }
+ #mysociety #drag img {
+ cursor: move; }
+ #mysociety #drag img.pin {
+ z-index: 100;
+ background-color: inherit; }
+ #mysociety #drag a img.pin {
+ cursor: pointer;
+ cursor: hand; }
#mysociety form#mapForm #map {
- cursor: pointer;
-}
+ cursor: pointer; }
#mysociety form#mapForm .olTileImage {
cursor: crosshair;
}
@@ -244,21 +205,18 @@
border-color: #fff #fff #fff #eee;
}
#mysociety #text_no_map {
- margin-top: 0;
-}
+ margin-top: 0; }
#mysociety #sub_map_links {
float: right;
clear: right;
- margin-top: 0;
-}
+ margin-top: 0; }
#mysociety #fixed {
margin: 0 530px 1em 0;
padding: 5px;
text-align: center;
position: relative;
background-color: #ccffcc;
- border: solid 2px #009900;
-}
+ border: solid 2px #009900; }
#mysociety #unknown {
margin: 0 530px 1em 0;
padding: 5px;
@@ -286,26 +244,21 @@
#mysociety #updates div {
padding: 0 0 0.5em;
margin: 0 0 0.25em;
- border-bottom: dotted 1px #5e552b;
-}
-#mysociety #updates div .problem-update, #mysociety #updates div .update-text {
- padding: 0;
- margin: 0;
- border-bottom: 0;
-}
+ border-bottom: dotted 1px #5e552b; }
+ #mysociety #updates div .problem-update, #mysociety #updates div .update-text {
+ padding: 0;
+ margin: 0;
+ border-bottom: 0; }
#mysociety #updates p {
- margin: 0;
-}
+ margin: 0; }
#mysociety #nearby_lists h2 {
margin-top: 1em;
margin-bottom: 0;
}
#mysociety #nearby_lists li small {
- color: #666666;
-}
+ color: #666666; }
#mysociety #alert_links {
- float: right;
-}
+ float: right; }
#mysociety #alert_links_area {
padding-left: 0.5em;
margin: 0;
@@ -313,11 +266,9 @@
font-size: smaller;
}
#mysociety #rss_alert {
- text-decoration: none;
-}
-#mysociety #rss_alert span {
- text-decoration: underline;
-}
+ text-decoration: none; }
+ #mysociety #rss_alert span {
+ text-decoration: underline; }
#mysociety #email_alert_box {
display: none;
position: absolute;
@@ -325,87 +276,67 @@
font-size: 83%;
border: solid 1px #7399C3;
background-color: #eeeeff;
- color: #000000;
-}
+ color: #000000; }
#mysociety #email_alert_box p {
- margin: 0;
-}
+ margin: 0; }
#mysociety .council_sent_info {
- font-size: smaller;
-}
+ font-size: smaller; }
#mysociety #rss_items {
width: 62%;
- float: left;
-}
+ float: left; }
#mysociety #rss_rhs {
border-left: 1px dashed #999;
width: 36%;
float: right;
padding: 0 0 0 0.5em;
- margin: 0 0 1em 0.5em;
-}
+ margin: 0 0 1em 0.5em; }
#mysociety #rss_box {
padding: 10px;
- border: 1px solid #999999;
-}
+ border: 1px solid #999999; }
#mysociety #rss_feed {
list-style-type: none;
- margin-bottom: 2em;
-}
+ margin-bottom: 2em; }
#mysociety #rss_feed li {
- margin-bottom: 1em;
-}
+ margin-bottom: 1em; }
#mysociety #alert_or {
font-style: italic;
font-size: 125%;
- margin: 0;
-}
+ margin: 0; }
#mysociety #rss_list {
float: left;
- width: 47%;
-}
+ width: 47%; }
#mysociety #rss_list ul {
- list-style-type: none;
-}
+ list-style-type: none; }
#mysociety #rss_buttons {
float: right;
width: 35%;
text-align: center;
- margin-bottom: 2em;
-}
+ margin-bottom: 2em; }
#mysociety #rss_local {
margin-left: 1.5em;
- margin-bottom: 0;
-}
+ margin-bottom: 0; }
#mysociety #rss_local_alt {
- margin: 0 0 2em 4em;
-}
+ margin: 0 0 2em 4em; }
#mysociety #alert_photos {
text-align: center;
float: right;
width: 150px;
- margin-left: 0.5em;
-}
+ margin-left: 0.5em; }
#mysociety #alert_photos h2 {
- font-size: 100%;
-}
+ font-size: 100%; }
#mysociety #alert_photos img {
- margin-bottom: 0.25em;
-}
+ margin-bottom: 0.25em; }
#mysociety #col_problems, #mysociety #col_fixed {
float: left;
width: 48%;
- margin-right: 1em;
-}
+ margin-right: 1em; }
#mysociety .contact-details {
font-size: 80%;
- margin-top: 2em;
-}
+ margin-top: 2em; }
.olControlAttribution {
bottom: 3px !important;
- left: 3px;
-}
+ left: 3px; }
.olControlPermalink {
bottom: 3px !important;
@@ -415,12 +346,8 @@
@media print {
#mysociety #map_box {
float: none;
- margin: 0 auto;
- }
+ margin: 0 auto; }
#mysociety #mysociety {
- max-width: none;
- }
+ max-width: none; }
#mysociety #side {
- margin-right: 0;
- }
-}
+ margin-right: 0; } }
diff --git a/web/css/core.scss b/web/css/core.scss
index 5fafe625b..b962b38ae 100644
--- a/web/css/core.scss
+++ b/web/css/core.scss
@@ -206,6 +206,14 @@ $map_width: 500px;
margin-bottom: 1em;
}
+ #category_meta {
+ margin-bottom: 30px;
+ }
+
+ #category_meta label {
+ width: 10em;
+ }
+
// Map
#map_box {
diff --git a/web/js/fixmystreet.js b/web/js/fixmystreet.js
index 4b19dc53e..9fa0f948b 100644
--- a/web/js/fixmystreet.js
+++ b/web/js/fixmystreet.js
@@ -57,4 +57,30 @@ $(function(){
timer = window.setTimeout(email_alert_close, 2000);
});
+ $('#form_category').change(function() {
+ if ( category_extras ) {
+ $('#category_meta').empty();
+ if ( category_extras[this.options[ this.selectedIndex ].text] ) {
+ var fields = category_extras[this.options[ this.selectedIndex ].text];
+ $('<h4>Additional information</h4>').appendTo('#category_meta');
+ for ( var i in fields) {
+ var meta = fields[i];
+ var field = '<div class="form-field">';
+ field += '<label for="form_' + meta.code + '">' + meta.description + ':</label>';
+ if ( meta.values ) {
+ field += '<select name="' + meta.code + '" id="form_' + meta.code + '">';
+ for ( var j in meta.values.value ) {
+ field += '<option value="' + meta.values.value[j].key + '">' + j + '</option>';
+ }
+ field += '</select>';
+ } else {
+ field += '<input type="text" value="" name="' + meta.code + '" id="form_' + meta.code + '">';
+ }
+ field += '</div>';
+ $( field ).appendTo('#category_meta');
+ }
+ }
+ }
+ });
+
});