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,