diff --git a/lib/NGCP/Panel/Controller/API/CustomerLocations.pm b/lib/NGCP/Panel/Controller/API/CustomerLocations.pm new file mode 100644 index 0000000000..cc6361989f --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CustomerLocations.pm @@ -0,0 +1,212 @@ +package NGCP::Panel::Controller::API::CustomerLocations; +use NGCP::Panel::Utils::Generic qw(:all); +no Moose; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + +use TryCatch; +use NGCP::Panel::Utils::ContractLocations qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET POST OPTIONS HEAD/]; +} + +sub api_description { + return 'A Customer Location is a container for a number of network ranges.'; +}; + +sub query_params { + return [ + { + param => 'ip', + description => 'Filter for customer locations containing a specific IP address', + query => { + first => \&NGCP::Panel::Utils::ContractLocations::prepare_query_param_value, + second => sub { + return { join => 'voip_contract_location_blocks', + group_by => 'me.id', } + #distinct => 1 }; #not neccessary if _CHECK_BLOCK_OVERLAPS was always on + }, + }, + }, + { + param => 'name', + description => 'Filter for customer locations matching a name pattern', + query => { + first => sub { + my $q = shift; + { name => { like => $q } }; + }, + second => sub {}, + }, + }, + ]; +} + + +use base qw/Catalyst::Controller NGCP::Panel::Role::API::CustomerLocations/; + +sub resource_name{ + return 'customerlocations'; +} +sub dispatch_path{ + return '/api/customerlocations/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-customerlocations'; +} + +__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(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 $cls = $self->item_rs($c); + + (my $total_count, $cls) = $self->paginate_order_collection($c, $cls); + my (@embedded, @links); + for my $cl ($cls->all) { + push @embedded, $self->hal_from_item($c, $cl, $self->resource_name); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('%s%d', $self->dispatch_path, $cl->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/'); + + push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params); + + 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 => ['contract_id'], + ); + + last unless $self->prepare_blocks_resource($c,$resource); + my $blocks = delete $resource->{blocks}; + + my $cl; + try { + $cl = $schema->resultset('voip_contract_locations')->create($resource); + for my $block (@$blocks) { + $cl->create_related('voip_contract_location_blocks', $block); + } + } catch($e) { + $c->log->error("failed to create customer location: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer location."); + return; + }; + + last unless $self->add_create_journal_item_hal($c,sub { + my $self = shift; + my ($c) = @_; + my $_cl = $self->item_by_id($c, $cl->id); + return $self->hal_from_item($c, $_cl, $self->resource_name); }); + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $cl->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/CustomerLocationsItem.pm b/lib/NGCP/Panel/Controller/API/CustomerLocationsItem.pm new file mode 100644 index 0000000000..ae30a3ed34 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CustomerLocationsItem.pm @@ -0,0 +1,232 @@ +package NGCP::Panel::Controller::API::CustomerLocationsItem; +use NGCP::Panel::Utils::Generic qw(:all); +no Moose; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); + +use TryCatch; +use NGCP::Panel::Utils::ValidateJSON qw(); +use NGCP::Panel::Utils::Reseller qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +sub allowed_methods{ + return [qw/GET OPTIONS HEAD PATCH PUT DELETE/]; +} + +use base qw/Catalyst::Controller NGCP::Panel::Role::API::CustomerLocations/; + +sub resource_name{ + return 'customerlocations'; +} +sub dispatch_path{ + return '/api/customerlocations/'; +} +sub relation{ + return 'http://purl.org/sipwise/ngcp-api/#rel-customerlocations'; +} + +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(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 $cl = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, customerlocation => $cl); + + my $hal = $self->hal_from_item($c, $cl, $self->resource_name); + + 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 $cl = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, customerlocation => $cl); + my $old_resource = $self->hal_from_item($c, $cl, $self->resource_name)->resource; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $cl = $self->update_item($c, $cl, $old_resource, $resource, $form); + last unless $cl; + + my $hal = $self->hal_from_item($c, $cl, $self->resource_name); + 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, $dset, "destinationsets"); + 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 $cl = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, customerlocation => $cl); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $cl->get_inflated_columns }; + + my $form = $self->get_form($c); + $cl = $self->update_item($c, $cl, $old_resource, $resource, $form); + last unless $cl; + + my $hal = $self->hal_from_item($c, $cl, $self->resource_name); + 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 $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 $cl = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, customerlocation => $cl); + try { + $cl->delete; + } catch($e) { + $c->log->error("Failed to delete customre location 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; +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/CustomerPreferences.pm b/lib/NGCP/Panel/Controller/API/CustomerPreferences.pm index 799a3935c3..9c06c8aed2 100644 --- a/lib/NGCP/Panel/Controller/API/CustomerPreferences.pm +++ b/lib/NGCP/Panel/Controller/API/CustomerPreferences.pm @@ -24,6 +24,15 @@ sub api_description { return 'Specifies certain properties (preferences) for a Customer. The full list of properties can be obtained via CustomerPreferenceDefs.'; }; +sub query_params { + return [ + { + param => 'location_id', + description => 'Fetch preferences for a specific location otherwise default preferences (location_id=null) are shown.', + }, + ]; +} + use base qw/Catalyst::Controller NGCP::Panel::Role::API::Preferences/; sub resource_name{ diff --git a/lib/NGCP/Panel/Controller/Customer.pm b/lib/NGCP/Panel/Controller/Customer.pm index 3a847217f4..94529891fc 100644 --- a/lib/NGCP/Panel/Controller/Customer.pm +++ b/lib/NGCP/Panel/Controller/Customer.pm @@ -16,6 +16,7 @@ use NGCP::Panel::Form::Customer::PbxGroupEdit; use NGCP::Panel::Form::Customer::PbxGroup; use NGCP::Panel::Form::Customer::PbxFieldDevice; use NGCP::Panel::Form::Customer::PbxFieldDeviceSync; +use NGCP::Panel::Form::Customer::Location; use NGCP::Panel::Form::Contract::Customer; use NGCP::Panel::Form::Contract::ProductSelect; use NGCP::Panel::Form::Topup::Cash; @@ -29,6 +30,7 @@ use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages; use NGCP::Panel::Utils::DeviceBootstrap; use NGCP::Panel::Utils::Voucher; +use NGCP::Panel::Utils::ContractLocations qw(); use Template; =head1 NAME @@ -409,6 +411,11 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { ->count; } + my $locations = $contract_rs->first->voip_contract_locations->search_rs( + undef, + { join => 'voip_contract_location_blocks', + group_by => 'me.id' }); + $c->stash->{invoice_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", search => 1, title => $c->loc("#") }, { name => "serial", search => 1, title => $c->loc("Serial #") }, @@ -419,6 +426,13 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { { name => "amount_total", search => 1, title => $c->loc("Total Amount") }, ]); + $c->stash->{location_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ + { name => "id", search => 1, title => $c->loc("#") }, + { name => "name", search => 1, title => $c->loc("Name") }, + { name => "description", search => 1, title => $c->loc("Description") }, + NGCP::Panel::Utils::ContractLocations::get_datatable_cols($c), + ]); + my ($is_timely,$timely_start,$timely_end) = NGCP::Panel::Utils::ProfilePackages::get_timely_range( package => $contract_first->profile_package, contract => $contract_first, @@ -446,6 +460,7 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { #$c->stash(now => $now ); $c->stash(billing_mappings_ordered_result => $billing_mappings_ordered ); $c->stash(future_billing_mappings => $future_billing_mappings ); + $c->stash(locations => $locations ); } sub base_restricted :Chained('base') :PathPart('') :CaptureArgs(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) { @@ -1908,13 +1923,225 @@ sub pbx_device_sync :Chained('pbx_device_base') :PathPart('sync') :Args(0) { ); } -sub preferences :Chained('base') :PathPart('preferences') :Args(0) { +sub location_ajax :Chained('base') :PathPart('location/ajax') :Args(0) { + my ($self, $c) = @_; + NGCP::Panel::Utils::Datatables::process($c, + @{$c->stash}{qw(locations location_dt_columns)}); + $c->detach( $c->view("JSON") ); +} + +sub location_create :Chained('base_restricted') :PathPart('location/create') :Args(0) { + my ($self, $c) = @_; + + my $contract = $c->stash->{contract}; + my $posted = ($c->request->method eq 'POST'); + my $form = NGCP::Panel::Form::Customer::Location->new; + my $params = {}; + $params = merge($params, $c->session->{created_objects}); + $form->process( + posted => $posted, + params => $c->request->params, + item => $params, + ); + NGCP::Panel::Utils::Navigation::check_form_buttons( + c => $c, + form => $form, + back_uri => $c->req->uri, + ); + if($posted && $form->validated) { + try { + $c->model('DB')->schema->txn_do( sub { + my $vcl = $c->model('DB')->resultset('voip_contract_locations')->create({ + contract_id => $c->stash->{contract}->id, + name => $form->values->{name}, + description => $form->values->{description}, + }); + for my $block (@{$form->values->{blocks}}) { + $vcl->create_related("voip_contract_location_blocks", $block); + } + $c->session->{created_objects}->{network} = { id => $vcl->id }; + }); + + NGCP::Panel::Utils::Message::info( + c => $c, + desc => $c->loc('Location successfully created'), + ); + } catch ($e) { + NGCP::Panel::Utils::Message::error( + c => $c, + error => $e, + desc => $c->loc('Failed to create location.'), + ); + } + NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for_action("/customer/details", [$contract->id])); + } + + $c->stash( + close_target => $c->uri_for_action("/customer/details", [$contract->id]), + create_flag => 1, + form => $form + ); +} + +sub location_base :Chained('base_restricted') :PathPart('location') :CaptureArgs(1) { + my ($self, $c, $location_id) = @_; + + unless($location_id && is_int($location_id)) { + $location_id //= ''; + NGCP::Panel::Utils::Message::error( + c => $c, + data => { id => $location_id }, + desc => $c->loc('Invalid location id detected'), + ); + $c->response->redirect($c->uri_for()); + $c->detach; + return; + } + + my $res = $c->stash->{contract}->voip_contract_locations->find($location_id); + unless(defined($res)) { + NGCP::Panel::Utils::Message::error( + c => $c, + desc => $c->loc('Location does not exist'), + ); + $c->response->redirect($c->uri_for()); + $c->detach; + return; + } + + $c->stash(location => {$res->get_inflated_columns}, + location_blocks => [ map { { $_->get_inflated_columns }; } + $res->voip_contract_location_blocks->all ], + location_result => $res); +} + +sub location_edit :Chained('location_base') :PathPart('edit') :Args(0) { + my ($self, $c) = @_; + + my $contract = $c->stash->{contract}; + my $posted = ($c->request->method eq 'POST'); + my $form = NGCP::Panel::Form::Customer::Location->new(ctx => $c); + my $params = $c->stash->{location}; + $params->{blocks} = $c->stash->{location_blocks}; + $params = merge($params, $c->session->{created_objects}); + $form->process( + posted => $posted, + params => $c->request->params, + item => $params, + ); + if($posted && $form->validated) { + try { + $c->model('DB')->schema->txn_do( sub { + + $c->stash->{'location_result'}->update({ + name => $form->values->{name}, + description => $form->values->{description}, + }); + $c->stash->{'location_result'}->voip_contract_location_blocks->delete; + for my $block (@{$form->values->{blocks}}) { + $c->stash->{'location_result'}->create_related("voip_contract_location_blocks", $block); + } + }); + NGCP::Panel::Utils::Message::info( + c => $c, + desc => $c->loc('Location successfully updated'), + ); + } catch ($e) { + NGCP::Panel::Utils::Message::error( + c => $c, + error => $e, + desc => $c->loc('Failed to update location'), + ); + } + + NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for_action("/customer/details", [$contract->id])); + + } + + $c->stash( + close_target => $c->uri_for_action("/customer/details", [$contract->id]), + edit_flag => 1, + form => $form + ); +} + +sub location_delete :Chained('location_base') :PathPart('delete') :Args(0) { + my ($self, $c) = @_; + + my $contract = $c->stash->{contract}; + my $location = $c->stash->{location_result}; + + try { + $location->delete; + NGCP::Panel::Utils::Message::info( + c => $c, + data => $c->stash->{location}, + desc => $c->loc('Location successfully deleted'), + ); + } catch ($e) { + NGCP::Panel::Utils::Message::error( + c => $c, + error => $e, + data => $c->stash->{location}, + desc => $c->loc('Failed to terminate location'), + ); + }; + + NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for_action("/customer/details", [$contract->id])); +} + +sub location_preferences :Chained('location_base') :PathPart('preferences') :Args(0) { my ($self, $c) = @_; $self->load_preference_list($c); $c->stash(template => 'customer/preferences.tt'); } +sub location_preferences_base :Chained('location_base') :PathPart('preferences') :CaptureArgs(1) { + my ($self, $c, $pref_id) = @_; + + $self->preferences_base($c, $pref_id); +} + +sub location_preferences_edit :Chained('location_preferences_base') :PathPart('edit') :Args(0) { + my ($self, $c) = @_; + + my $contract = $c->stash->{contract}; + my $location = $c->stash->{location}; + my $pref_id = $c->stash->{pref_id}; + + my $base_uri = $c->uri_for($contract->id, + 'location', $location->{id}, + 'preferences'); + my $edit_uri = $c->uri_for($contract->id, + 'location', $location->{id}, + 'preferences', $pref_id, + 'edit'); + + $c->stash(edit_preference => 1); + + my @enums = $c->stash->{preference_meta} + ->voip_preferences_enums + ->search({contract_pref => 1}) + ->all; + + my $pref_rs = $contract->voip_contract_preferences( + { location_id => $location->{id} }, undef); + + NGCP::Panel::Utils::Preferences::create_preference_form( c => $c, + pref_rs => $pref_rs, + enums => \@enums, + base_uri => $base_uri, + edit_uri => $edit_uri, + ); +} + +sub preferences :Chained('base') :PathPart('preferences') :Args(0) { + my ($self, $c) = @_; + + $self->load_preference_list($c); + $c->stash(template => 'customer/preferences.tt'); +} sub preferences_base :Chained('base') :PathPart('preferences') :CaptureArgs(1) { my ($self, $c, $pref_id) = @_; @@ -1934,8 +2161,10 @@ sub preferences_base :Chained('base') :PathPart('preferences') :CaptureArgs(1) { ->search({ attribute_id => $pref_id, contract_id => $c->stash->{contract}->id, + location_id => $c->stash->{location}{id} || undef, }); my @values = $c->stash->{preference}->get_column("value")->all; + $c->stash->{pref_id} = $pref_id; $c->stash->{preference_values} = \@values; $c->stash(template => 'customer/preferences.tt'); } @@ -1950,13 +2179,17 @@ sub preferences_edit :Chained('preferences_base') :PathPart('edit') :Args(0) { ->search({contract_pref => 1}) ->all; - my $pref_rs = $c->stash->{contract}->voip_contract_preferences; + my $pref_rs = $c->stash->{contract}->voip_contract_preferences( + { location_id => undef }, undef); + + my $base_uri = $c->uri_for_action('/customer/preferences', [$c->stash->{contract}->id]); + my $edit_uri = $c->uri_for_action('/customer/preferences_edit', [$c->stash->{contract}->id]); NGCP::Panel::Utils::Preferences::create_preference_form( c => $c, pref_rs => $pref_rs, enums => \@enums, - base_uri => $c->uri_for_action('/customer/preferences', [$c->stash->{contract}->id]), - edit_uri => $c->uri_for_action('/customer/preferences_edit', [$c->stash->{contract}->id]), + base_uri => $base_uri, + edit_uri => $edit_uri, ); } @@ -1967,6 +2200,8 @@ sub load_preference_list :Private { ->resultset('voip_preferences') ->search({ contract_id => $c->stash->{contract}->id, + 'voip_contract_preferences.location_id' => + $c->stash->{location}{id} || undef, },{ prefetch => 'voip_contract_preferences', }); diff --git a/lib/NGCP/Panel/Form/Customer/Location.pm b/lib/NGCP/Panel/Form/Customer/Location.pm new file mode 100644 index 0000000000..206c3eb2ce --- /dev/null +++ b/lib/NGCP/Panel/Form/Customer/Location.pm @@ -0,0 +1,179 @@ +package NGCP::Panel::Form::Customer::Location; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; +#use Moose::Util::TypeConstraints; + +use NGCP::Panel::Utils::ContractLocations qw(); +use Storable qw(); + +use HTML::FormHandler::Widget::Block::Bootstrap; + +with 'NGCP::Panel::Render::RepeatableJs'; + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +has_field 'submitid' => ( type => 'Hidden' ); +sub build_render_list {[qw/submitid fields actions/]} +sub build_form_element_class {[qw(form-horizontal)]} + +has_field 'id' => ( + type => 'Hidden', +); + +has_field 'name' => ( + type => 'Text', + label => 'Location Name', + required => 1, + maxlength => 255, + element_attr => { + rel => ['tooltip'], + title => ['Name of the location.'] + }, +); + +has_field 'description' => ( + type => 'Text', + label => 'Description', + required => 1, + maxlength => 255, + element_attr => { + rel => ['tooltip'], + title => ['Arbitrary text.'], + }, +); + +has_field 'blocks' => ( + type => 'Repeatable', + required => 1, + setup_for_js => 1, + do_wrapper => 1, + do_label => 0, + tags => { + controls_div => 1, + }, + wrapper_class => [qw/hfh-rep/], + element_attr => { + rel => ['tooltip'], + title => ['An array of location blocks, each containing the keys (base) "ip" address and an optional "mask" to specify the network portion (subnet prefix length).'], + }, + validate_method => \&_validate_blocks, + inflate_default_method => \&_inflate_blocks, +); + +has_field 'blocks.row' => ( + type => 'Compound', + label => 'Location Block', + do_label => 1, + tags => { + before_element => '