diff options
22 files changed, 965 insertions, 197 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/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 9c5b3d8fd..3ec3c6756 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -79,6 +79,9 @@ create table contacts ( whenedited timestamp not null, -- what the last change was for: author's notes note text not null + + -- extra fields required for open311 + extra text ); create unique index contacts_area_id_category_idx on contacts(area_id, category); @@ -176,6 +179,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 ); create index problem_state_latitude_longitude_idx on problem(state, latitude, longitude); create index problem_user_id_idx on problem ( user_id ); @@ -387,3 +391,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 3854e27aa..59695db70 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -323,6 +323,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'); + } } } @@ -340,6 +366,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 346dfb377..b48c76651 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 @@ -449,6 +450,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' ) { @@ -495,8 +497,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; } @@ -511,6 +517,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{$_} } # @@ -689,6 +697,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 2df26fde3..567ae9388 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 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->has_many( @@ -100,8 +102,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( @@ -111,11 +113,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'; diff --git a/perllib/Open311.pm b/perllib/Open311.pm new file mode 100644 index 000000000..0c8c3fde6 --- /dev/null +++ b/perllib/Open311.pm @@ -0,0 +1,179 @@ +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' ); + +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 $service_request_xml = $self->_get( 'requests.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 ); + $uri->path( $uri->path . $path ); + $uri->query_form( jurisdiction_id => $self->jurisdiction ); + + my $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/t/app/controller/admin.t b/t/app/controller/admin.t index 4e2ec82fe..a7eee4dd3 100644 --- a/t/app/controller/admin.t +++ b/t/app/controller/admin.t @@ -202,6 +202,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/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/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.html b/templates/web/default/report/new/fill_in_details.html index 8150ba894..874445abd 100644 --- a/templates/web/default/report/new/fill_in_details.html +++ b/templates/web/default/report/new/fill_in_details.html @@ -102,6 +102,35 @@ <textarea name="detail" id="form_detail" rows="7" cols="26">[% report.detail | html %]</textarea> </div> +[%- 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 44c60ed6e..d55ec5826 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 6dc75e6a1..ac29dfe7a 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,131 +71,108 @@ -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; -} + cursor: crosshair; } #mysociety #compass { background-color: #ffffff; border: solid 1px #000000; @@ -220,76 +180,60 @@ color: #000000; position: absolute; top: 0px; - left: 0px; -} -#mysociety #compass img { - border: 0; -} + left: 0px; } + #mysociety #compass img { + border: 0; } #mysociety #text_map { margin-top: 0.5em; margin-bottom: 1em; - font-size: 110%; -} + font-size: 110%; } #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; text-align: center; position: relative; background-color: #ffcccc; - border: solid 2px #990000; -} + border: solid 2px #990000; } #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: 0.5em; - margin-bottom: 0; -} + margin-bottom: 0; } #mysociety #nearby_lists li small { - color: #666666; -} + color: #666666; } #mysociety #alert_links { - float: right; -} + float: right; } #mysociety #alert_links_area { background-color: #ffeecc; border: solid 1px #ff9900; border-width: 1px 0; padding: 3px 10px; - margin: 0; -} + margin: 0; } #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; @@ -297,97 +241,73 @@ 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; } @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 55fda5a31..4e8d732d5 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'); + } + } + } + }); + }); |