From af86cac8276539da2f44fc2ec57aff9588a8212a Mon Sep 17 00:00:00 2001 From: Gerhard Jungwirth Date: Tue, 30 Aug 2016 13:39:55 +0200 Subject: [PATCH] TT#2393 implement a-number cf for API callforward based on source-numbers in rest-api Change-Id: I94dcd67f09dd027e444894d2b549b9901eee7c22 --- lib/NGCP/Panel/Controller/API/CFMappings.pm | 5 +- lib/NGCP/Panel/Controller/API/CFSourceSets.pm | 253 ++++++++++++++++++ .../Panel/Controller/API/CFSourceSetsItem.pm | 239 +++++++++++++++++ lib/NGCP/Panel/Controller/API/CallForwards.pm | 6 +- lib/NGCP/Panel/Form/CFMappingsAPI.pm | 44 ++- lib/NGCP/Panel/Form/CFSimpleAPI.pm | 60 ++++- lib/NGCP/Panel/Form/CFSourceSetAPI.pm | 52 ++++ lib/NGCP/Panel/Role/API/CFMappings.pm | 12 + lib/NGCP/Panel/Role/API/CFSourceSets.pm | 147 ++++++++++ lib/NGCP/Panel/Role/API/CallForwards.pm | 33 ++- t/api-rest/api-root.t | 1 + 11 files changed, 828 insertions(+), 24 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/CFSourceSets.pm create mode 100644 lib/NGCP/Panel/Controller/API/CFSourceSetsItem.pm create mode 100644 lib/NGCP/Panel/Form/CFSourceSetAPI.pm create mode 100644 lib/NGCP/Panel/Role/API/CFSourceSets.pm diff --git a/lib/NGCP/Panel/Controller/API/CFMappings.pm b/lib/NGCP/Panel/Controller/API/CFMappings.pm index 06b77f5fdc..f2fa968c00 100644 --- a/lib/NGCP/Panel/Controller/API/CFMappings.pm +++ b/lib/NGCP/Panel/Controller/API/CFMappings.pm @@ -21,8 +21,8 @@ sub allowed_methods{ sub api_description { return 'Specifies callforward mappings of a subscriber, where multiple mappings can be specified per type (cfu, cfb, cft cfna) ' . - 'One mapping consists of destinationset name (see CFDestinationSets) and a timeset name ' . - '(see CFTimeSets).'; + 'Each mapping consists of a destinationset name (see CFDestinationSets), a timeset name ' . + '(see CFTimeSets) and a sourceset name (see CFSourceSets).'; } sub query_params { @@ -35,6 +35,7 @@ sub documentation_sample { cfb => [{ "destinationset" => "quickset_cfb", "timeset" => undef, + "sourceset" => undef, }], cfna => [], cft => [], diff --git a/lib/NGCP/Panel/Controller/API/CFSourceSets.pm b/lib/NGCP/Panel/Controller/API/CFSourceSets.pm new file mode 100644 index 0000000000..401c09c6f6 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CFSourceSets.pm @@ -0,0 +1,253 @@ +package NGCP::Panel::Controller::API::CFSourceSets; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + +use NGCP::Panel::Utils::DateTime; +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require NGCP::Panel::Role::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET POST OPTIONS HEAD/]; +} + +sub api_description { + return 'Defines a collection of CallForward Source Sets, including their source, which can be set '. + 'to define CallForwards using CFMappings.',; +} + +sub query_params { + return [ + { + param => 'subscriber_id', + description => 'Filter for source sets belonging to a specific subscriber', + query => { + first => sub { + my $q = shift; + return { 'voip_subscriber.id' => $q }; + }, + second => sub { + return { join => {subscriber => 'voip_subscriber'}}; + }, + }, + }, + { + param => 'name', + description => 'Filter for items matching a source set name pattern', + query => { + first => sub { + my $q = shift; + { name => { like => $q } }; + }, + second => sub {}, + }, + }, + ]; +} + +sub documentation_sample { + return { + subscriber_id => 20, + name => 'from_alice', + sources => [{source => 'alice'}], + }; +} + +use parent qw/Catalyst::Controller NGCP::Panel::Role::API::CFSourceSets/; + +sub resource_name{ + return 'cfsourcesets'; +} +sub dispatch_path{ + return '/api/cfsourcesets/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-cfsourcesets'; +} + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }, + }, + action_roles => [qw(+NGCP::Panel::Role::HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); + return 1; +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $ssets = $self->item_rs($c); + + (my $total_count, $ssets) = $self->paginate_order_collection($c, $ssets); + my (@embedded, @links); + for my $sset ($ssets->all) { + push @embedded, $self->hal_from_item($c, $sset, "cfsourcesets"); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('%s%d', $self->dispatch_path, $sset->id), + ); + } + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf('%s?page=%s&rows=%s', $self->dispatch_path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page + 1, $rows)); + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page - 1, $rows)); + } + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], + ); + $hal->resource({ + total_count => $total_count, + }); + my $response = HTTP::Response->new(HTTP_OK, undef, + HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub POST :Allow { + my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $schema = $c->model('DB'); + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + exceptions => [ "subscriber_id" ], + ); + + my $sset; + + unless(defined $resource->{subscriber_id}) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Missing mandatory field 'subscriber_id'"); + last; + } + + my $b_subscriber = $schema->resultset('voip_subscribers')->find({ + id => $resource->{subscriber_id}, + }); + unless($b_subscriber) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'subscriber_id'."); + last; + } + my $subscriber = $b_subscriber->provisioning_voip_subscriber; + unless($subscriber) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid subscriber."); + last; + } + if (! exists $resource->{sources} ) { + $resource->{sources} = []; + } + if (ref $resource->{sources} ne "ARRAY") { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'sources'. Must be an array."); + last; + } + try { + my $domain = $subscriber->domain->domain // ''; + + $sset = $schema->resultset('voip_cf_source_sets')->create({ + name => $resource->{name}, + subscriber_id => $subscriber->id, + }); + for my $s ( @{$resource->{sources}} ) { + $sset->create_related("voip_cf_sources", { + source => $s->{source}, + }); + } + } catch($e) { + $c->log->error("failed to create cfsourceset: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create cfsourceset."); + last; + } + + last unless $self->add_create_journal_item_hal($c,sub { + my $self = shift; + my ($c) = @_; + my $_sset = $self->item_by_id($c, $sset->id); + return $self->hal_from_item($c, $_sset, "cfsourcesets"); }); + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $sset->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/CFSourceSetsItem.pm b/lib/NGCP/Panel/Controller/API/CFSourceSetsItem.pm new file mode 100644 index 0000000000..abb084261b --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CFSourceSetsItem.pm @@ -0,0 +1,239 @@ +package NGCP::Panel::Controller::API::CFSourceSetsItem; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + +use NGCP::Panel::Utils::ValidateJSON qw(); +use NGCP::Panel::Utils::DateTime; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +require Catalyst::ActionRole::ACL; +require NGCP::Panel::Role::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET OPTIONS HEAD PATCH PUT DELETE/]; +} + +use parent qw/Catalyst::Controller NGCP::Panel::Role::API::CFSourceSets/; + +sub resource_name{ + return 'cfsourcesets'; +} +sub dispatch_path{ + return '/api/cfsourcesets/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-cfsourcesets'; +} + +sub journal_query_params { + my($self,$query_params) = @_; + return $self->get_journal_query_params($query_params); +} + +__PACKAGE__->config( + action => { + (map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 1, + Does => [qw(ACL RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }), + @{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{ + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Does => [qw(ACL RequireSSL)], + }) } + }, + action_roles => [qw(+NGCP::Panel::Role::HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); + return 1; +} + +sub GET :Allow { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $sset = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, sourceset => $sset); + + my $hal = $self->hal_from_item($c, $sset, "cfsourcesets"); + + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + (map { # XXX Data::HAL must be able to generate links with multiple relations + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r + =~ s/rel=self/rel="item self"/r; + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c, $id) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub PATCH :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ops => [qw/add replace remove copy/], + ); + last unless $json; + + my $sset = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, sourceset => $sset); + my $old_resource = $self->hal_from_item($c, $sset, "cfsourcesets")->resource; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $sset = $self->update_item($c, $sset, $old_resource, $resource, $form); + last unless $sset; + + my $hal = $self->hal_from_item($c, $sset, "cfsourcesets"); + last unless $self->add_update_journal_item_hal($c,$hal); + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + #my $hal = $self->hal_from_item($c, $sset, "sourcesets"); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $sset = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, sourceset => $sset); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $sset->get_inflated_columns }; + + my $form = $self->get_form($c); + $sset = $self->update_item($c, $sset, $old_resource, $resource, $form); + last unless $sset; + + my $hal = $self->hal_from_item($c, $sset, "cfsourcesets"); + last unless $self->add_update_journal_item_hal($c,$hal); + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + #my $hal = $self->hal_from_item($c, $sset, "sourcesets"); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub DELETE :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $sset = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, sourceset => $sset); + + last unless $self->add_delete_journal_item_hal($c,sub { + my $self = shift; + my ($c) = @_; + return $self->hal_from_item($c, $sset, "cfsourcesets"); }); + + try { + $sset->delete; + } catch($e) { + $c->log->error("Failed to delete cfsourceset with id '$id': $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"); + last; + } + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub get_journal_methods{ + return [qw/handle_item_base_journal handle_journals_get handle_journalsitem_get handle_journals_options handle_journalsitem_options handle_journals_head handle_journalsitem_head/]; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/CallForwards.pm b/lib/NGCP/Panel/Controller/API/CallForwards.pm index 7bc82ea027..7481c5a708 100644 --- a/lib/NGCP/Panel/Controller/API/CallForwards.pm +++ b/lib/NGCP/Panel/Controller/API/CallForwards.pm @@ -22,8 +22,9 @@ sub allowed_methods{ } sub api_description { - return 'Specifies basic callforwards of a subscriber, where a number of destinations and times can be specified for each type (cfu, cfb, cft cfna). '. - 'For more complex configurations with multiple combinations of Timesets and Destinationsets see CFMappings.'; + return 'Specifies basic callforwards of a subscriber, where a number of destinations, times and sources ' . + ' can be specified for each type (cfu, cfb, cft cfna). For more complex configurations with ' . + ' multiple combinations of Timesets, Destinationsets and SourceSets see CFMappings.'; }; sub query_params { @@ -39,6 +40,7 @@ sub documentation_sample { "timeout" => "300", }], "times" => [], + "sources" => [], }, cfna => {}, cft => { "ringtimeout" => "199" }, diff --git a/lib/NGCP/Panel/Form/CFMappingsAPI.pm b/lib/NGCP/Panel/Form/CFMappingsAPI.pm index 90655e5174..49474f41fa 100644 --- a/lib/NGCP/Panel/Form/CFMappingsAPI.pm +++ b/lib/NGCP/Panel/Form/CFMappingsAPI.pm @@ -19,8 +19,9 @@ has_field 'cfu' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Unconditional, Number of Objects, each containing the keys, "destinationset" and ' . - '"timeset". The values must be the name of a destination/time set which belongs to the same subscriber.'], + title => ['Call Forward Unconditional, Number of Objects, each containing the keys ' . + '"destinationset", "timeset" and "sourceset". The values must be the name of ' . + 'a corresponding set which belongs to the same subscriber.'], }, ); @@ -31,8 +32,9 @@ has_field 'cfb' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Busy, Number of Objects, each containing the keys, "destinationset" and ' . - '"timeset". The values must be the name of a destination/time set which belongs to the same subscriber.'], + title => ['Call Forward Busy, Number of Objects, each containing the keys ' . + '"destinationset", "timeset" and "sourceset". The values must be the name of ' . + 'a corresponding set which belongs to the same subscriber.'], }, ); @@ -43,8 +45,9 @@ has_field 'cft' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Timeout, Number of Objects, each containing the keys, "destinationset" and ' . - '"timeset". The values must be the name of a destination/time set which belongs to the same subscriber.'], + title => ['Call Forward Timeout, Number of Objects, each containing the keys ' . + '"destinationset", "timeset" and "sourceset". The values must be the name of ' . + 'a corresponding set which belongs to the same subscriber.'], }, ); @@ -55,8 +58,9 @@ has_field 'cfna' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Unavailable, Number of Objects, each containing the keys, "destinationset" and ' . - '"timeset". The values must be the name of a destination/time set which belongs to the same subscriber.'], + title => ['Call Forward Unavailable, Number of Objects, each containing the keys ' . + '"destinationset", "timeset" and "sourceset". The values must be the name of ' . + 'a corresponding set which belongs to the same subscriber.'], }, ); @@ -72,6 +76,12 @@ has_field 'cfu.timeset' => ( do_label => 0, ); +has_field 'cfu.sourceset' => ( + type => 'Text', + do_wrapper => 1, + do_label => 0, +); + has_field 'cfb.destinationset' => ( type => 'Text', do_wrapper => 1, @@ -84,6 +94,12 @@ has_field 'cfb.timeset' => ( do_label => 0, ); +has_field 'cfb.sourceset' => ( + type => 'Text', + do_wrapper => 1, + do_label => 0, +); + has_field 'cft.destinationset' => ( type => 'Text', do_wrapper => 1, @@ -96,6 +112,12 @@ has_field 'cft.timeset' => ( do_label => 0, ); +has_field 'cft.sourceset' => ( + type => 'Text', + do_wrapper => 1, + do_label => 0, +); + has_field 'cfna.destinationset' => ( type => 'Text', do_wrapper => 1, @@ -108,6 +130,12 @@ has_field 'cfna.timeset' => ( do_label => 0, ); +has_field 'cfna.sourceset' => ( + type => 'Text', + do_wrapper => 1, + do_label => 0, +); + has_field 'cft_ringtimeout' => ( type => 'PosInteger', do_wrapper => 1, diff --git a/lib/NGCP/Panel/Form/CFSimpleAPI.pm b/lib/NGCP/Panel/Form/CFSimpleAPI.pm index b575ec7735..c5d594dd49 100644 --- a/lib/NGCP/Panel/Form/CFSimpleAPI.pm +++ b/lib/NGCP/Panel/Form/CFSimpleAPI.pm @@ -19,9 +19,10 @@ has_field 'cfu' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Unconditional, Contains the keys "destinations" and "times". "destinations" is an Array of Objects ' . + title => ['Call Forward Unconditional, Contains the keys "destinations", "times" and "sources". "destinations" is an Array of Objects ' . 'having a "destination", "priority" and "timeout" field. "times" is an Array of Objects having the fields ' . - '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always.'], + '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always. ' . + '"sources" is an Array of Objects having one field "source". "sources" can be empty.'], }, ); @@ -32,9 +33,10 @@ has_field 'cfb' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Busy, Contains the keys "destinations" and "times". "destinations" is an Array of Objects ' . + title => ['Call Forward Busy, Contains the keys "destinations", "times" and "sources". "destinations" is an Array of Objects ' . 'having a "destination", "priority" and "timeout" field. "times" is an Array of Objects having the fields ' . - '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always.'], + '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always. ' . + '"sources" is an Array of Objects having one field "source". "sources" can be empty.'], }, ); @@ -45,9 +47,10 @@ has_field 'cft' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Timeout, Contains the keys "destinations", "times" and "ringtimeout". "destinations" is an Array of Objects ' . + title => ['Call Forward Timeout, Contains the keys "destinations", "times", "sources" and "ringtimeout". "destinations" is an Array of Objects ' . 'having a "destination", "priority" and "timeout" field. "times" is an Array of Objects having the fields ' . - '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always.'. + '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always. ' . + '"sources" is an Array of Objects having one field "source". "sources" can be empty.' . '"ringtimeout" is a numeric ringing time value in seconds before call forward will be applied.'], }, ); @@ -59,9 +62,10 @@ has_field 'cfna' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['Call Forward Unavailable, Contains the keys "destinations" and "times". "destinations" is an Array of Objects ' . + title => ['Call Forward Unavailable, Contains the keys "destinations", "times" and "sources". "destinations" is an Array of Objects ' . 'having a "destination", "priority" and "timeout" field. "times" is an Array of Objects having the fields ' . - '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always.'], + '"minute", "hour", "wday", "mday", "month", "year". "times" can be empty, then the CF is applied always. ' . + '"sources" is an Array of Objects having one field "source". "sources" can be empty.'], }, ); @@ -85,6 +89,16 @@ has_field 'cfu.times' => ( do_label => 0, ); +has_field 'cfu.sources' => ( + type => 'Repeatable', + do_wrapper => 1, + do_label => 0, +); + +has_field 'cfu.sources.source' => ( + type => 'Text', +); + has_field 'cfb.destinations' => ( type => 'Repeatable', do_wrapper => 1, @@ -105,6 +119,16 @@ has_field 'cfb.times' => ( do_label => 0, ); +has_field 'cfb.sources' => ( + type => 'Repeatable', + do_wrapper => 1, + do_label => 0, +); + +has_field 'cfb.sources.source' => ( + type => 'Text', +); + has_field 'cft.destinations' => ( type => 'Repeatable', do_wrapper => 1, @@ -125,6 +149,16 @@ has_field 'cft.times' => ( do_label => 0, ); +has_field 'cft.sources' => ( + type => 'Repeatable', + do_wrapper => 1, + do_label => 0, +); + +has_field 'cft.sources.source' => ( + type => 'Text', +); + has_field 'cfna.destinations' => ( type => 'Repeatable', do_wrapper => 1, @@ -145,6 +179,16 @@ has_field 'cfna.times' => ( do_label => 0, ); +has_field 'cfna.sources' => ( + type => 'Repeatable', + do_wrapper => 1, + do_label => 0, +); + +has_field 'cfna.sources.source' => ( + type => 'Text', +); + has_field 'cft.ringtimeout' => ( type => 'PosInteger', do_wrapper => 1, diff --git a/lib/NGCP/Panel/Form/CFSourceSetAPI.pm b/lib/NGCP/Panel/Form/CFSourceSetAPI.pm new file mode 100644 index 0000000000..e983ea5fc8 --- /dev/null +++ b/lib/NGCP/Panel/Form/CFSourceSetAPI.pm @@ -0,0 +1,52 @@ +package NGCP::Panel::Form::CFSourceSetAPI; +use HTML::FormHandler::Moose; +use HTML::FormHandler::Widget::Block::Bootstrap; +extends 'HTML::FormHandler'; + +has_field 'id' => ( + type => 'Hidden', +); + +has_field 'subscriber_id' => ( + type => 'PosInteger', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The subscriber id this source set belongs to'] + }, +); + +has_field 'name' => ( + type => 'Text', + label => 'Name', + wrapper_class => [qw/hfh-rep-field/], + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The name of the source set'] + }, +); + +has_field 'sources' => ( + type => 'Repeatable', + element_attr => { + rel => ['tooltip'], + title => ['An array of sources, each containing the key "source" ' . + 'which will be matched against the call\'s anumber to determine ' . + 'whether to apply the callforward or not.', + ] + }, +); + +has_field 'sources.id' => ( + type => 'Hidden', +); + +has_field 'sources.source' => ( + type => 'Text', + label => 'Source', +); + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/CFMappings.pm b/lib/NGCP/Panel/Role/API/CFMappings.pm index a2a596d4c0..9bc7c5f66f 100644 --- a/lib/NGCP/Panel/Role/API/CFMappings.pm +++ b/lib/NGCP/Panel/Role/API/CFMappings.pm @@ -35,9 +35,11 @@ sub hal_from_item { for my $mapping ($item->provisioning_voip_subscriber->voip_cf_mappings->all) { my $dset = $mapping->destination_set ? $mapping->destination_set->name : undef; my $tset = $mapping->time_set ? $mapping->time_set->name : undef; + my $sset = $mapping->source_set ? $mapping->source_set->name : undef; push @{ $resource->{$mapping->type} }, { destinationset => $dset, timeset => $tset, + sourceset => $sset, }; } @@ -121,6 +123,7 @@ sub update_item { my %cf_preferences; my $dsets_rs = $c->model('DB')->resultset('voip_cf_destination_sets'); my $tsets_rs = $c->model('DB')->resultset('voip_cf_time_sets'); + my $ssets_rs = $c->model('DB')->resultset('voip_cf_source_sets'); for my $type ( qw/cfu cfb cft cfna/) { if (ref $resource->{$type} ne "ARRAY") { @@ -148,9 +151,18 @@ sub update_item { return; } } + my $sset; + if ($mapping->{sourceset}) { + $sset = $ssets_rs->find({subscriber_id => $p_subs_id, name => $mapping->{sourceset}, }); + unless ($sset) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'sourceset'. Could not be found."); + return; + } + } push @new_mappings, $mappings_rs->new_result({ destination_set_id => $dset->id, time_set_id => $tset ? $tset->id : undef, + source_set_id => $sset ? $sset->id : undef, type => $type, }); } diff --git a/lib/NGCP/Panel/Role/API/CFSourceSets.pm b/lib/NGCP/Panel/Role/API/CFSourceSets.pm new file mode 100644 index 0000000000..09cd6afb5f --- /dev/null +++ b/lib/NGCP/Panel/Role/API/CFSourceSets.pm @@ -0,0 +1,147 @@ +package NGCP::Panel::Role::API::CFSourceSets; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use parent 'NGCP::Panel::Role::API'; + + +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use JSON::Types; +use NGCP::Panel::Utils::Subscriber; +use NGCP::Panel::Form::CFSourceSetAPI; + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::CFSourceSetAPI->new; +} + +sub hal_from_item { + my ($self, $c, $item, $type) = @_; + my $form; + + my %resource = $item->get_inflated_columns; + my @sources; + for my $dest ($item->voip_cf_sources->all) { + push @sources, { $dest->get_inflated_columns, }; + delete @{$sources[-1]}{'source_set_id', 'id'}; + } + $resource{sources} = \@sources; + + my $b_subs_id = $item->subscriber->voip_subscriber->id; + $resource{subscriber_id} = $b_subs_id; + + my $hal = Data::HAL->new( + links => [ + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'collection', href => sprintf("%s", $self->dispatch_path)), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), + Data::HAL::Link->new(relation => "ngcp:$type", href => sprintf("/api/%s/%d", $type, $item->id)), + Data::HAL::Link->new(relation => "ngcp:subscribers", href => sprintf("/api/subscribers/%d", $b_subs_id)), + $self->get_journal_relation_link($item->id), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => \%resource, + run => 0, + exceptions => [ "subscriber_id" ], + ); + $hal->resource(\%resource); + return $hal; +} + +sub _item_rs { + my ($self, $c) = @_; + my $item_rs; + + if($c->user->roles eq "admin") { + $item_rs = $c->model('DB')->resultset('voip_cf_source_sets'); + } elsif ($c->user->roles eq "reseller") { + my $reseller_id = $c->user->reseller_id; + $item_rs = $c->model('DB')->resultset('voip_cf_source_sets') + ->search_rs({ + 'reseller_id' => $reseller_id, + } , { + join => {'subscriber' => {'contract' => 'contact'} }, + }); + } + + return $item_rs; +} + +sub item_by_id { + my ($self, $c, $id) = @_; + + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + delete $resource->{id}; + my $schema = $c->model('DB'); + + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + exceptions => [ "subscriber_id" ], + ); + + if (! exists $resource->{sources} ) { + $resource->{sources} = []; + } + if (ref $resource->{sources} ne "ARRAY") { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'sources'. Must be an array."); + return; + } + + my $b_subscriber = $schema->resultset('voip_subscribers')->find($resource->{subscriber_id}); + unless ($b_subscriber) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'subscriber_id'."); + return; + } + my $subscriber = $b_subscriber->provisioning_voip_subscriber; + unless($subscriber) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid subscriber."); + last; + } + + try { + $item->update({ + name => $resource->{name}, + subscriber_id => $subscriber->id, + })->discard_changes; + $item->voip_cf_sources->delete; + for my $s ( @{$resource->{sources}} ) { + $item->create_related("voip_cf_sources", { + source => $s->{source}, + }); + } + $item->discard_changes; + } catch($e) { + $c->log->error("failed to create cfsourceset: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create cfsourceset."); + return; + }; + + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/CallForwards.pm b/lib/NGCP/Panel/Role/API/CallForwards.pm index f9daa8da80..f3a3e68261 100644 --- a/lib/NGCP/Panel/Role/API/CallForwards.pm +++ b/lib/NGCP/Panel/Role/API/CallForwards.pm @@ -47,7 +47,7 @@ sub hal_from_item { relation => 'ngcp:'.$self->resource_name, ); @resource{qw/cfu cfb cft cfna/} = ({}) x 4; - for my $item_cf ($item->provisioning_voip_subscriber->voip_cf_mappings->all){ + for my $item_cf ($item->provisioning_voip_subscriber->voip_cf_mappings->all) { $resource{$item_cf->type} = $self->_contents_from_cfm($c, $item_cf, $item); } if(keys %{$resource{cft}}){ @@ -130,7 +130,7 @@ sub update_item { my $mapping_count = $mapping->count; my $cf_preference = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( c => $c, prov_subscriber => $prov_subs, attribute => $type); - my ($dset, $tset); + my ($dset, $tset, $sset); if ($mapping_count == 0) { next unless (defined $resource->{$type}); $mapping = $c->model('DB')->resultset('voip_cf_mappings')->create({ @@ -145,6 +145,7 @@ sub update_item { $mapping = $mapping->first; $dset = $mapping->destination_set; $tset = $mapping->time_set; + $sset = $mapping->source_set; } try { @@ -191,6 +192,21 @@ sub update_item { $mapping->update({time_set_id => $tset->id}); } } + if ($sset) { + if ((defined $resource->{$type}{sources}) && @{ $resource->{$type}{sources}}) { + $sset->voip_cf_sources->delete; #empty sset + } else { + $mapping_count && $mapping->update({source_set_id => undef}); + if ($sset->name =~ m/^quickset_/) { + $tset->delete; # delete sset + } + } + } else { + if ((defined $resource->{$type}{times}) && @{ $resource->{$type}{times}}) { + $sset = $mapping->create_related('source_set', {'name' => "quickset_$type", subscriber_id => $prov_subscriber_id,} ); + $mapping->update({source_set_id => $sset->id}); + } + } for my $d (@{ $resource->{$type}{destinations} }) { delete $d->{destination_set_id}; delete $d->{simple_destination}; @@ -207,6 +223,10 @@ sub update_item { delete $t->{time_set_id}; $tset->voip_cf_periods->update_or_create($t); } + for my $s (@{ $resource->{$type}{sources} }) { + delete $s->{source_set_id}; + $sset->voip_cf_sources->update_or_create($s); + } $dset->discard_changes if $dset; # update destinations my $new_autoattendant = NGCP::Panel::Utils::Subscriber::check_dset_autoattendant_status($dset); @@ -249,9 +269,10 @@ sub update_item { sub _contents_from_cfm { my ($self, $c, $cfm_item, $sub) = @_; - my (@times, @destinations); + my (@times, @destinations, @sources); my $timeset_item = $cfm_item->time_set; my $dset_item = $cfm_item->destination_set; + my $sourceset_item = $cfm_item->source_set; for my $time ($timeset_item ? $timeset_item->voip_cf_periods->all : () ) { push @times, {$time->get_inflated_columns}; delete @{$times[-1]}{'time_set_id', 'id'}; @@ -269,7 +290,11 @@ sub _contents_from_cfm { }; delete @{$destinations[-1]}{'destination_set_id', 'id'}; } - return {times => \@times, destinations => \@destinations}; + for my $source ($sourceset_item ? $sourceset_item->voip_cf_sources->all : () ) { + push @sources, {$source->get_inflated_columns}; + delete @{$sources[-1]}{'source_set_id', 'id'}; + } + return {times => \@times, destinations => \@destinations, sources => \@sources}; } 1; diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index 758c150f8e..ad763c2818 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -60,6 +60,7 @@ $ua->credentials($netloc, "api_admin_http", $user, $pass); ccmapentries => 1, cfdestinationsets => 1, cfmappings => 1, + cfsourcesets => 1, cftimesets => 1, contracts => 1, customerbalances => 1,