diff options
-rw-r--r-- | perllib/Open311/Endpoint.pm | 17 | ||||
-rw-r--r-- | perllib/Open311/Endpoint/Role/mySociety.pm | 152 | ||||
-rw-r--r-- | perllib/Open311/Endpoint/Schema.pm | 3 | ||||
-rw-r--r-- | perllib/Open311/Endpoint/Service/Request.pm | 3 | ||||
-rw-r--r-- | perllib/Open311/Endpoint/Service/Request/Update.pm | 57 | ||||
-rw-r--r-- | perllib/Open311/Endpoint/Service/Request/mySociety.pm | 52 | ||||
-rw-r--r-- | perllib/Open311/Endpoint/Spark.pm | 3 | ||||
-rw-r--r-- | t/open311/endpoint/Endpoint1.pm | 5 | ||||
-rw-r--r-- | t/open311/endpoint/Endpoint2.pm | 29 | ||||
-rw-r--r-- | t/open311/endpoint/ServiceType1.pm | 2 | ||||
-rw-r--r-- | t/open311/endpoint/mysociety.t | 161 |
11 files changed, 478 insertions, 6 deletions
diff --git a/perllib/Open311/Endpoint.pm b/perllib/Open311/Endpoint.pm index 8a440e4ae..0abb52aab 100644 --- a/perllib/Open311/Endpoint.pm +++ b/perllib/Open311/Endpoint.pm @@ -38,7 +38,11 @@ and provide the necessary methods. =head1 SUBCLASSING -See also t/open311/endpoint/Endpoint1.pm as an example. + package My::Open311::Endpoint; + use Web::Simple; + extends 'Open311::Endpoint'; + +See also t/open311/endpoint/Endpoint1.pm and Endpoint2.pm as examples. =head2 methods to override @@ -202,6 +206,11 @@ has schema => ( }, ); +sub learn_additional_types { + # my ($self, $rx) = @_; + ## no-op, but override in ::Role or implementation! +} + has spark => ( is => 'lazy', default => sub { @@ -232,6 +241,12 @@ has w3_dt => ( default => sub { DateTime::Format::W3CDTF->new }, ); +sub maybe_inflate_datetime { + my ($self, $dt) = @_; + return unless $dt; + return $self->w3_dt->parse_datetime($dt); +} + =head2 Dispatching The method dispatch_request returns a list of all the dispatcher routines diff --git a/perllib/Open311/Endpoint/Role/mySociety.pm b/perllib/Open311/Endpoint/Role/mySociety.pm new file mode 100644 index 000000000..42afe73bb --- /dev/null +++ b/perllib/Open311/Endpoint/Role/mySociety.pm @@ -0,0 +1,152 @@ +package Open311::Endpoint::Role::mySociety; + +=head1 NAME + +Open311::Endpoint::Role::mySociety - mySociety's proposed Open311 extensions + +=head1 SYNOPSIS + +See mySociety's +L<blog post|https://www.mysociety.org/2013/02/20/open311-extended/> +and +L<proposal|https://github.com/mysociety/FixMyStreet/wiki/Open311-FMS---Proposed-differences-to-Open311> +for a full explanation of the spec extension. + +You can use the extensions as follows: + + package My::Open311::Endpoint; + use Web::Simple; + extends 'Open311::Endpoint'; + with 'Open311::Endpoint::Role::mySociety'; + +You will have to provide implementations of + + get_service_request_updates + post_service_request_update + +You will need to return L<Open311::Endpoint::Service::Request::Update> +objects. However, the root L<Open311::Endpoint::Service::Request> is not +aware of updates, so you may may find it easier to ensure that the ::Service +objects you create (with get_service_request etc.) return +L<Open311::Endpoint::Service::Request::mySociety> objects. + +=cut + +use Moo::Role; +no warnings 'illegalproto'; + +around dispatch_request => sub { + my ($orig, $self, @args) = @_; + my @dispatch = $self->$orig(@args); + return ( + @dispatch, + + sub (GET + /servicerequestupdates + ?*) { + my ($self, $args) = @_; + $self->call_api( GET_Service_Request_Updates => $args ); + }, + + sub (POST + /servicerequestupdates + ?*) { + my ($self, $args) = @_; + $self->call_api( POST_Service_Request_Update => $args ); + }, + + ); +}; + +sub GET_Service_Request_Updates_input_schema { + my $self = shift; + return { + type => '//rec', + required => { + $self->get_jurisdiction_id_required_clause, + }, + optional => { + $self->get_jurisdiction_id_optional_clause,, + start_date => '/open311/datetime', + end_date => '/open311/datetime', + } + }; +} + +sub GET_Service_Request_Updates_output_schema { + my $self = shift; + return { + type => '//rec', + required => { + service_request_updates => { + type => '//arr', + contents => '/open311/service_request_update', + }, + }, + }; +} + +sub GET_Service_Request_Updates { + my ($self, $args) = @_; + + my @updates = $self->get_service_request_updates({ + jurisdiction_id => $args->{jurisdiction_id}, + start_date => $args->{start_date}, + end_date => $args->{end_date}, + }); + + $self->format_updates(@updates); +} + +sub format_updates { + my ($self, @updates) = @_; + return { + service_request_updates => [ + map { + my $update = $_; + +{ + ( + map { + $_ => $update->$_, + } + qw/ + update_id + service_request_id + status + description + media_url + / + ), + ( + map { + $_ => $self->w3_dt->format_datetime( $update->$_ ), + } + qw/ + updated_datetime + / + ), + } + } @updates + ] + }; +} + +sub get_service_request_updates { + my ($self, $args) = @_; + die "abstract method get_service_request_updates not overridden"; +} + +sub learn_additional_types { + my ($self, $schema) = @_; + $schema->learn_type( 'tag:wiki.open311.org,GeoReport_v2:rx/service_request_update', + { + type => '//rec', + required => { + service_request_id => $self->get_identifier_type('service_request_id'), + update_id => $self->get_identifier_type('update_id'), + status => '/open311/status', + updated_datetime => '/open311/datetime', + description => '//str', + media_url => '//str', + }, + } + ); +} + +1; diff --git a/perllib/Open311/Endpoint/Schema.pm b/perllib/Open311/Endpoint/Schema.pm index e30f9ad90..9a2ad81e5 100644 --- a/perllib/Open311/Endpoint/Schema.pm +++ b/perllib/Open311/Endpoint/Schema.pm @@ -12,6 +12,7 @@ has endpoint => ( get_jurisdiction_id_required_clause get_jurisdiction_id_optional_clause get_identifier_type + learn_additional_types /], ); @@ -167,6 +168,8 @@ has schema => ( } ); + $self->learn_additional_types($schema); + return $schema; }, ); diff --git a/perllib/Open311/Endpoint/Service/Request.pm b/perllib/Open311/Endpoint/Service/Request.pm index b56cee393..1d835f2d5 100644 --- a/perllib/Open311/Endpoint/Service/Request.pm +++ b/perllib/Open311/Endpoint/Service/Request.pm @@ -1,5 +1,6 @@ package Open311::Endpoint::Service::Request; use Moo; +use MooX::HandlesVia; use Types::Standard ':all'; use namespace::clean; @@ -57,7 +58,7 @@ has requested_datetime => ( ); has updated_datetime => ( - is => 'ro', + is => 'rw', isa => Maybe[ InstanceOf['DateTime'] ], ); diff --git a/perllib/Open311/Endpoint/Service/Request/Update.pm b/perllib/Open311/Endpoint/Service/Request/Update.pm new file mode 100644 index 000000000..b881af9ce --- /dev/null +++ b/perllib/Open311/Endpoint/Service/Request/Update.pm @@ -0,0 +1,57 @@ +package Open311::Endpoint::Service::Request::Update; +use Moo; +use Types::Standard ':all'; +use namespace::clean; + +sub BUILDARGS { + my ($class, %args) = @_; + my $service_request = delete $args{service_request}; + + if (! $args{status}) { + $args{status} = $service_request->status; + } + + return \%args; +} + +has update_id => ( + is => 'ro', + isa => Maybe[Str], + predicate => 1, +); + +has service_request_id => ( + is => 'ro', + isa => Maybe[Str], + predicate => 1, +); + +has token => ( + is => 'ro', + isa => Maybe[Str], + predicate => 1, +); + +has status => ( + is => 'ro', + isa => Enum[qw/ open closed /], +); + +has description => ( + is => 'ro', + isa => Maybe[Str], +); + +has media_url => ( + is => 'ro', + isa => Str, + default => sub { '' }, +); + +has updated_datetime => ( + is => 'ro', + isa => InstanceOf['DateTime'], + default => sub { DateTime->now() }, +); + +1; diff --git a/perllib/Open311/Endpoint/Service/Request/mySociety.pm b/perllib/Open311/Endpoint/Service/Request/mySociety.pm new file mode 100644 index 000000000..748065ad8 --- /dev/null +++ b/perllib/Open311/Endpoint/Service/Request/mySociety.pm @@ -0,0 +1,52 @@ +package Open311::Endpoint::Service::Request::mySociety; +use Moo; +use MooX::HandlesVia; +extends 'Open311::Endpoint::Service::Request'; + +use DateTime; +use Open311::Endpoint::Service::Request::Update; +use Types::Standard ':all'; + +has updates => ( + is => 'rw', + isa => ArrayRef[InstanceOf['Open311::Endpoint::Service::Request::Update']], + default => sub { [] }, + handles_via => 'Array', + handles => { + _add_update => 'push', + get_updates => 'elements', + get_update => 'get', + has_updates => 'count', + filter_updates => 'grep', + } +); + +sub add_update { + my ($self, %args) = @_; + my $update = Open311::Endpoint::Service::Request::Update->new( + %args, + service_request => $self, + service_request_id => $self->service_request_id, + ); + $self->_add_update($update); +} + + +sub last_update { + my $self = shift; + return $self->has_updates ? $self->get_update(-1) : undef; +} + +around updated_datetime => sub { + my ($orig, $self) = @_; + my $last_update = $self->last_update or return; + return $last_update->updated_datetime; +}; + +around status => sub { + my ($orig, $self) = @_; + my $last_update = $self->last_update or return 'open'; + return $last_update->status; +}; + +1; diff --git a/perllib/Open311/Endpoint/Spark.pm b/perllib/Open311/Endpoint/Spark.pm index ae179cecc..292a66996 100644 --- a/perllib/Open311/Endpoint/Spark.pm +++ b/perllib/Open311/Endpoint/Spark.pm @@ -1,6 +1,5 @@ package Open311::Endpoint::Spark; use Moo; -use Data::Visitor::Callback; =head1 NAME @@ -102,7 +101,9 @@ sub _process_for_xml { my %singular_map = ( service_requests => 'request', + service_request_updates => 'request_update', ); + sub _singularize { my $name = shift; return $singular_map{ $name } diff --git a/t/open311/endpoint/Endpoint1.pm b/t/open311/endpoint/Endpoint1.pm index ccd16c238..257a94b64 100644 --- a/t/open311/endpoint/Endpoint1.pm +++ b/t/open311/endpoint/Endpoint1.pm @@ -7,6 +7,9 @@ use MooX::HandlesVia; use Open311::Endpoint::Service; use t::open311::endpoint::ServiceType1; use Open311::Endpoint::Service::Attribute; +use Open311::Endpoint::Service::Request; + +use constant request_class => 'Open311::Endpoint::Service::Request'; sub services { return ( @@ -73,7 +76,7 @@ sub services { sub post_service_request { my ($self, $service, $args) = @_; - my $request = Open311::Endpoint::Service::Request->new( + my $request = $self->request_class->new( # NB: possible race condition between next_request_id and _add_request # (this is fine for synchronous test-cases) diff --git a/t/open311/endpoint/Endpoint2.pm b/t/open311/endpoint/Endpoint2.pm new file mode 100644 index 000000000..2b55b1e10 --- /dev/null +++ b/t/open311/endpoint/Endpoint2.pm @@ -0,0 +1,29 @@ +package t::open311::endpoint::Endpoint2; +use Web::Simple; +extends 't::open311::endpoint::Endpoint1'; +with 'Open311::Endpoint::Role::mySociety'; + +use Open311::Endpoint::Service::Request::mySociety; + +use constant request_class => 'Open311::Endpoint::Service::Request::mySociety'; + +sub get_service_request_updates { + my ($self, $args) = @_; + + my $start_date = $self->maybe_inflate_datetime($args->{start_date}); + my $end_date = $self->maybe_inflate_datetime($args->{end_date}); + + my @requests = $self->filter_requests( sub { $_[0]->has_updates } ); + + return map { + $_->filter_updates( sub { + my $update = shift; + my $updated_datetime = $update->updated_datetime or return; + if ($start_date) { return unless $updated_datetime >= $start_date } + if ($end_date) { return unless $updated_datetime <= $end_date } + return 1; + }); + } @requests; +} + +1; diff --git a/t/open311/endpoint/ServiceType1.pm b/t/open311/endpoint/ServiceType1.pm index e73b15b7d..e35e44fda 100644 --- a/t/open311/endpoint/ServiceType1.pm +++ b/t/open311/endpoint/ServiceType1.pm @@ -3,8 +3,6 @@ use Moo; extends 'Open311::Endpoint::Service'; use DateTime; -use Open311::Endpoint::Service::Request; - has '+default_service_notice' => ( default => 'This is a test service', ); diff --git a/t/open311/endpoint/mysociety.t b/t/open311/endpoint/mysociety.t new file mode 100644 index 000000000..ea4f72820 --- /dev/null +++ b/t/open311/endpoint/mysociety.t @@ -0,0 +1,161 @@ +use strict; use warnings; + +use Test::More; +use Test::LongString; +use Test::MockTime ':all'; + +use Open311::Endpoint; +use Data::Dumper; +use JSON; + +use t::open311::endpoint::Endpoint2; + +my $endpoint = t::open311::endpoint::Endpoint2->new; +my $json = JSON->new; + +subtest "POST OK" => sub { + diag "Serves as sanity test of subclassing, as well as preparing our data"; + # TODO, refactor repeated code lifted from t/open311/endpoint.t + + set_fixed_time('2014-01-01T12:00:00Z'); + my $res = $endpoint->run_test_request( + POST => '/requests.json', + api_key => 'test', + service_code => 'POT', + address_string => '22 Acacia Avenue', + first_name => 'Bob', + last_name => 'Mould', + 'attribute[depth]' => 100, + 'attribute[shape]' => 'triangle', + ); + ok $res->is_success, 'valid request' + or diag $res->content; + + is_deeply $json->decode($res->content), + [ { + "service_notice" => "This is a test service", + "service_request_id" => 0 + } ], 'correct json returned'; + + set_fixed_time('2014-02-01T12:00:00Z'); + $res = $endpoint->run_test_request( + POST => '/requests.xml', + api_key => 'test', + service_code => 'POT', + address_string => '22 Acacia Avenue', + first_name => 'Bob', + last_name => 'Mould', + 'attribute[depth]' => 100, + 'attribute[shape]' => 'triangle', + ); + + ok $res->is_success, 'valid request' + or diag $res->content; +}; + +subtest "GET Service Request Updates" => sub { + + my $empty_xml = <<CONTENT; +<?xml version="1.0" encoding="utf-8"?> +<service_request_updates> +</service_request_updates> +CONTENT + + my $update_0_xml = <<CONTENT; +<?xml version="1.0" encoding="utf-8"?> +<service_request_updates> + <request_updates> + <description>Fixed</description> + <media_url></media_url> + <service_request_id>0</service_request_id> + <status>closed</status> + <update_id>1</update_id> + <updated_datetime>2014-01-01T13:00:00Z</updated_datetime> + </request_updates> +</service_request_updates> +CONTENT + +my $updates_xml = <<CONTENT; +<?xml version="1.0" encoding="utf-8"?> +<service_request_updates> + <request_updates> + <description>Fixed</description> + <media_url></media_url> + <service_request_id>0</service_request_id> + <status>closed</status> + <update_id>1</update_id> + <updated_datetime>2014-01-01T13:00:00Z</updated_datetime> + </request_updates> + <request_updates> + <description>Have investigated. Looks tricky!</description> + <media_url></media_url> + <service_request_id>1</service_request_id> + <status>open</status> + <update_id>2</update_id> + <updated_datetime>2014-03-01T13:00:00Z</updated_datetime> + </request_updates> +</service_request_updates> +CONTENT + + subtest 'No updates' => sub { + my $res = $endpoint->run_test_request( GET => '/servicerequestupdates.xml', ); + ok $res->is_success, 'valid request' or diag $res->content; + + is_string $res->content, $empty_xml, 'xml string ok' + or diag $res->content; + }; + + subtest 'Updated 1 ticket' => sub { + # an agent updates the first ticket + set_fixed_time('2014-01-01T13:00:00Z'); + my $request = $endpoint->get_request(0); + $request->add_update( + update_id => 1, + status => 'closed', + description => 'Fixed', + ); + + is $request->status, 'closed', 'Status updated'; + + my $before='2014-01-01T12:00:00Z'; + my $after ='2014-01-01T14:00:00Z'; + + for my $scenario ( + [ '', $update_0_xml, 'Basic test', ], + [ "?start_date=$before", $update_0_xml, 'start date' ], + [ "?end_date=$after", $update_0_xml, 'end_date' ], + [ "?start_date=$before&end_date=$after", $update_0_xml, 'both dates' ], + [ "?start_date=$after", $empty_xml, 'Not found if start date after update' ], + [ "?end_date=$before", $empty_xml, 'Not found if end date before update' ] + ) { + my ($query, $xml, $description) = @$scenario; + + my $res = $endpoint->run_test_request( GET => '/servicerequestupdates.xml' . $query, ); + ok $res->is_success, 'valid request' or diag $res->content; + is_string $res->content, $xml, $description; + } + }; + + subtest 'Updated another ticket' => sub { + set_fixed_time('2014-03-01T13:00:00Z'); + my $request = $endpoint->get_request(1); + $request->add_update( + update_id => 2, + description => 'Have investigated. Looks tricky!', + ); + + for my $scenario ( + [ '', $updates_xml, 'Both reports', ], + [ "?end_date=2014-01-01T14:00:00Z", $update_0_xml, 'end_date before second update' ], + ) { + my ($query, $xml, $description) = @$scenario; + + my $res = $endpoint->run_test_request( GET => '/servicerequestupdates.xml' . $query, ); + ok $res->is_success, 'valid request' or diag $res->content; + is_string $res->content, $xml, $description or diag $res->content; + } + }; +}; + +restore_time(); +done_testing; |