MT#11147 Phone extensions

Change-Id: Ie08f85030a26dc00fe246c71e73a81bd001a2be4
changes/73/1373/1
Irina Peshinskaya 10 years ago
parent c52e8da48f
commit ec9c71dbe3

@ -10,6 +10,7 @@ use MooseX::ClassAttribute qw(class_has);
use Data::Dumper;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::DeviceBootstrap;
use NGCP::Panel::Utils::Device;
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::CheckTrailingSlash;
@ -202,6 +203,7 @@ sub POST :Allow {
last unless $self->valid_media_type($c, 'multipart/form-data');
last unless $self->require_wellformed_json($c, 'application/json', $c->req->param('json'));
my $resource = JSON::from_json($c->req->param('json'));
$resource->{type} //= 'phone';
$resource->{front_image} = $self->get_upload($c, 'front_image');
last unless $resource->{front_image};
# optional, don't set error
@ -258,6 +260,7 @@ sub POST :Allow {
}
try {
my $connectable_models = delete $resource->{connectable_models};
my $sync_parameters = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_prefetch($c, undef, $resource);
my $credentials = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_prefetch($c, undef, $resource);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_clear($c, $resource);
@ -265,6 +268,7 @@ sub POST :Allow {
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_store($c, $item, $credentials);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_store($c, $item, $sync_parameters);
NGCP::Panel::Utils::DeviceBootstrap::dispatch_devmod($c, 'register_model', $item);
NGCP::Panel::Utils::Device::process_connectable_models($c, 1, $item, $connectable_models );
foreach my $range(@{ $linerange }) {
unless(ref $range eq "HASH") {

@ -11,6 +11,7 @@ use NGCP::Panel::Utils::ValidateJSON qw();
use NGCP::Panel::Utils::DateTime;
use Path::Tiny qw(path);
use Safe::Isa qw($_isa);
use Clone qw/clone/;
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::HTTPMethods;
@ -102,6 +103,8 @@ sub PATCH :Allow {
my $item = $self->item_by_id($c, $id);
last unless $self->resource_exists($c, pbxdevicemodel => $item);
my $old_resource = $self->resource_from_item($c, $item);
#without it error: The entity could not be processed: Modification of a read-only value attempted at /usr/share/perl5/JSON/Pointer.pm line 200, <$fh> line 1.\n
$old_resource = clone($old_resource);
my $resource = $self->apply_patch($c, $old_resource, $json);
last unless $resource;

@ -1346,13 +1346,15 @@ sub pbx_device_lines_update :Private{
$err = 1;
last;
} else {
my ($range_id, $key_num) = split /\./, $line->field('line')->value;
my ($range_id, $key_num, $unit_short) = split /\./, $line->field('line')->value;
my $type = $line->field('type')->value;
my $unit = $line->field('extension_unit')->value || $unit_short || 0;
$fdev->autoprov_field_device_lines->create({
subscriber_id => $prov_subscriber->id,
linerange_id => $range_id,
key_num => $key_num,
line_type => $type,
subscriber_id => $prov_subscriber->id,
linerange_id => $range_id,
key_num => $key_num,
line_type => $type,
extension_unit => $unit,
});
}
}

@ -3,6 +3,7 @@ use Sipwise::Base;
use Template;
use Crypt::Rijndael;
use JSON qw(decode_json encode_json);
use NGCP::Panel::Form::Device::Model;
use NGCP::Panel::Form::Device::ModelAdmin;
use NGCP::Panel::Form::Device::Firmware;
@ -10,7 +11,7 @@ use NGCP::Panel::Form::Device::Config;
use NGCP::Panel::Form::Device::Profile;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::DeviceBootstrap;
use NGCP::Panel::Utils::Device;
BEGIN { extends 'Catalyst::Controller'; }
@ -35,16 +36,17 @@ sub base :Chained('/') :PathPart('device') :CaptureArgs(0) {
}
my $devmod_rs = $c->model('DB')->resultset('autoprov_devices')->search_rs(undef,{
'columns' => [qw/id reseller_id vendor model front_image_type mac_image_type num_lines bootstrap_method bootstrap_uri/],
'columns' => [qw/id reseller_id type vendor model front_image_type mac_image_type num_lines bootstrap_method bootstrap_uri extensions_num/],
});
$reseller_id and $devmod_rs = $devmod_rs->search({ reseller_id => $reseller_id });
$c->stash->{devmod_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => 'id', search => 1, title => $c->loc('#') },
{ name => 'type', search => 1, title => $c->loc('Type') },
{ name => 'reseller.name', search => 1, title => $c->loc('Reseller') },
{ name => 'vendor', search => 1, title => $c->loc('Vendor') },
{ name => 'model', search => 1, title => $c->loc('Model') },
]);
my $devfw_rs = $c->model('DB')->resultset('autoprov_firmwares')->search_rs(undef,{'columns' => [qw/id device_id version filename/],
});
$reseller_id and $devfw_rs = $devfw_rs->search({
@ -106,14 +108,28 @@ sub base :Chained('/') :PathPart('device') :CaptureArgs(0) {
{ name => 'contract.contact.email', search => 1, title => $c->loc('Customer Email') },
]);
my $extensions_rs = $c->model('DB')->resultset('autoprov_devices')->search_rs({
'type' => 'extension',
});
$reseller_id and $extensions_rs = $extensions_rs->search({ reseller_id => $reseller_id });
$c->stash->{fielddev_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => 'id', search => 1, title => $c->loc('#') },
{ name => 'identifier', search => 1, title => $c->loc('MAC Address / Identifier') },
{ name => 'profile.name', search => 1, title => $c->loc('Profile Name') },
{ name => 'contract.id', search => 1, title => $c->loc('Customer #') },
{ name => 'contract.contact.email', search => 1, title => $c->loc('Customer Email') },
]);
$c->stash(
devmod_rs => $devmod_rs,
devfw_rs => $devfw_rs,
devconf_rs => $devconf_rs,
devprof_rs => $devprof_rs,
fielddev_rs => $fielddev_rs,
reseller_id => $reseller_id,
template => 'device/list.tt',
devmod_rs => $devmod_rs,
devfw_rs => $devfw_rs,
devconf_rs => $devconf_rs,
devprof_rs => $devprof_rs,
fielddev_rs => $fielddev_rs,
extensions_rs => $extensions_rs,
reseller_id => $reseller_id,
template => 'device/list.tt',
);
}
@ -165,32 +181,31 @@ sub devmod_create :Chained('base') :PathPart('model/create') :Args(0) :Does(ACL)
my $schema = $c->model('DB');
$schema->txn_do(sub {
if($c->user->is_superuser) {
$form->params->{reseller_id} = $form->params->{reseller}{id};
$form->values->{reseller_id} = $form->values->{reseller}{id};
} else {
$form->params->{reseller_id} = $c->user->reseller_id;
$form->values->{reseller_id} = $c->user->reseller_id;
}
delete $form->params->{reseller};
delete $form->values->{reseller};
my $ft = File::Type->new();
if($form->params->{front_image}) {
my $front_image = delete $form->params->{front_image};
$form->params->{front_image} = $front_image->slurp;
$form->params->{front_image_type} = $ft->mime_type($form->params->{front_image});
}
if($form->params->{mac_image}) {
my $mac_image = delete $form->params->{mac_image};
$form->params->{mac_image} = $mac_image->slurp;
$form->params->{mac_image_type} = $ft->mime_type($form->params->{mac_image});
foreach(qw/front_image mac_image/){
if($form->values->{$_}) {
my $image = delete $form->values->{$_};
$form->values->{$_} = $image->slurp;
$form->values->{$_.'_type'} = $ft->mime_type($form->values->{$_});
}
}
my $linerange = delete $form->params->{linerange};
my $connectable_models = delete $form->values->{connectable_models};
my $linerange = delete $form->values->{linerange};
my $sync_parameters = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_prefetch($c, undef, $form->params);
my $credentials = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_prefetch($c, undef, $form->params);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_clear($c, $form->params);
my $devmod = $schema->resultset('autoprov_devices')->create($form->params);
my $sync_parameters = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_prefetch($c, undef, $form->values);
my $credentials = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_prefetch($c, undef, $form->values);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_clear($c, $form->values);
my $devmod = $schema->resultset('autoprov_devices')->create($form->values);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_store($c, $devmod, $credentials);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_store($c, $devmod, $sync_parameters);
NGCP::Panel::Utils::DeviceBootstrap::dispatch_devmod($c, 'register_model', $devmod);
NGCP::Panel::Utils::Device::process_connectable_models($c, 1, $devmod, decode_json($connectable_models) );
foreach my $range(@{ $linerange }) {
delete $range->{id};
@ -205,7 +220,6 @@ sub devmod_create :Chained('base') :PathPart('model/create') :Args(0) :Does(ACL)
$r->annotations->create($label);
}
}
delete $c->session->{created_objects}->{reseller};
$c->session->{created_objects}->{device} = { id => $devmod->id };
});
@ -228,6 +242,18 @@ sub devmod_create :Chained('base') :PathPart('model/create') :Args(0) :Does(ACL)
form => $form,
);
}
sub prepare_connectable :Private{
my ($self, $c, $model) = @_;
my $values = [];
my $connected_rs = $c->model('DB')->resultset('autoprov_device_extensions')->search_rs({
( $model->type eq 'phone' ? 'device_id' : 'extension_id' ) => $model->id,
});
for my $connected($connected_rs->all) {
push @$values, $connected->get_column ( $model->type eq 'phone' ? 'extension_id' : 'device_id' ) ;
}
return (encode_json($values), $values);
}
sub devmod_base :Chained('base') :PathPart('model') :CaptureArgs(1) {
my ($self, $c, $devmod_id) = @_;
@ -240,9 +266,8 @@ sub devmod_base :Chained('base') :PathPart('model') :CaptureArgs(1) {
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/device'));
}
$c->stash->{devmod} = $c->stash->{devmod_rs}->find($devmod_id,{'+columns' => [qw/mac_image front_image/]});
unless($c->stash->{devmod}) {
my $devmod = $c->stash->{devmod_rs}->find($devmod_id,{'+columns' => [qw/mac_image front_image/]});
unless($devmod) {
NGCP::Panel::Utils::Message->error(
c => $c,
error => "device model with id '$devmod_id' not found",
@ -250,6 +275,9 @@ sub devmod_base :Chained('base') :PathPart('model') :CaptureArgs(1) {
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/device'));
}
$c->stash(
devmod => $devmod,
);
}
sub devmod_delete :Chained('devmod_base') :PathPart('delete') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
@ -305,6 +333,8 @@ sub devmod_edit :Chained('devmod_base') :PathPart('edit') :Args(0) :Does(ACL) :A
$params->{'bootstrap_config_'.$c->stash->{devmod}->bootstrap_method.'_'.$_} = $credentials_rs->first->get_column($_);
}
}
#edit specific
($params->{connectable_models}) = $self->prepare_connectable($c, $c->stash->{devmod});
$params->{reseller}{id} = delete $params->{reseller_id};
$params = $params->merge($c->session->{created_objects});
@ -338,36 +368,30 @@ sub devmod_edit :Chained('devmod_base') :PathPart('edit') :Args(0) :Does(ACL) :A
my $schema = $c->model('DB');
$schema->txn_do(sub {
if($c->user->is_superuser) {
$form->params->{reseller_id} = $form->params->{reseller}{id};
$form->values->{reseller_id} = $form->values->{reseller}{id};
} else {
$form->params->{reseller_id} = $c->user->reseller_id;
$form->values->{reseller_id} = $c->user->reseller_id;
}
delete $form->params->{reseller};
delete $form->values->{reseller};
if($form->params->{front_image}) {
my $front_image = delete $form->params->{front_image};
$form->params->{front_image} = $front_image->slurp;
$form->params->{front_image_type} = $front_image->type;
} else {
delete $form->params->{front_image};
delete $form->params->{front_image_type};
}
if($form->params->{mac_image}) {
my $mac_image = delete $form->params->{mac_image};
$form->params->{mac_image} = $mac_image->slurp;
$form->params->{mac_image_type} = $mac_image->type;
} else {
delete $form->params->{mac_image};
delete $form->params->{mac_image_type};
foreach (qw/front_image mac_image/){
if($form->values->{$_}) {
my $image = delete $form->values->{$_};
$form->values->{$_} = $image->slurp;
$form->values->{$_.'_type'} = $image->type;
} else {
delete $form->values->{$_};
delete $form->values->{$_.'_type'};
}
}
my $linerange = delete $form->params->{linerange};
my $sync_parameters = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_prefetch($c, $c->stash->{devmod}, $form->params);
my $credentials = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_prefetch($c, $c->stash->{devmod}, $form->params);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_clear($c, $form->params);
my $linerange = delete $form->values->{linerange};
my $connectable_models = delete $form->values->{connectable_models};
my $sync_parameters = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_prefetch($c, $c->stash->{devmod}, $form->values);
my $credentials = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_prefetch($c, $c->stash->{devmod}, $form->values);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_clear($c, $form->values);
$c->stash->{devmod}->update($form->params);
$c->stash->{devmod}->update($form->values);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_store($c, $c->stash->{devmod}, $credentials);
$schema->resultset('autoprov_sync')->search_rs({
@ -375,6 +399,7 @@ sub devmod_edit :Chained('devmod_base') :PathPart('edit') :Args(0) :Does(ACL) :A
})->delete;
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_store($c, $c->stash->{devmod}, $sync_parameters);
NGCP::Panel::Utils::DeviceBootstrap::dispatch_devmod($c, 'register_model', $c->stash->{devmod} );
NGCP::Panel::Utils::Device::process_connectable_models($c, 0, $c->stash->{devmod}, decode_json($connectable_models) );
my @existing_range = ();
my $range_rs = $c->stash->{devmod}->autoprov_device_line_ranges;
@ -429,7 +454,6 @@ sub devmod_edit :Chained('devmod_base') :PathPart('edit') :Args(0) :Does(ACL) :A
$range_rs->search({
id => { 'not in' => \@existing_range },
})->delete_all;
delete $c->session->{created_objects}->{reseller};
});
NGCP::Panel::Utils::Message->info(
@ -531,11 +555,11 @@ sub devfw_create :Chained('base') :PathPart('firmware/create') :Args(0) :Does(AC
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
my $file = delete $form->params->{data};
$form->params->{filename} = $file->filename;
$form->params->{data} = $file->slurp;
my $devmod = $c->stash->{devmod_rs}->find($form->params->{device}{id},{'+columns' => [qw/mac_image front_image/]});
my $devfw = $devmod->create_related('autoprov_firmwares', $form->params);
my $file = delete $form->values->{data};
$form->values->{filename} = $file->filename;
$form->values->{data} = $file->slurp;
my $devmod = $c->stash->{devmod_rs}->find($form->values->{device}{id},{'+columns' => [qw/mac_image front_image/]});
my $devfw = $devmod->create_related('autoprov_firmwares', $form->values);
delete $c->session->{created_objects}->{device};
$c->session->{created_objects}->{firmware} = { id => $devfw->id };
});
@ -634,13 +658,13 @@ sub devfw_edit :Chained('devfw_base') :PathPart('edit') :Args(0) {
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
$form->params->{device_id} = $form->params->{device}{id};
delete $form->params->{device};
my $file = delete $form->params->{data};
$form->params->{filename} = $file->filename;
$form->params->{data} = $file->slurp;
$form->values->{device_id} = $form->values->{device}{id};
delete $form->values->{device};
my $file = delete $form->values->{data};
$form->values->{filename} = $file->filename;
$form->values->{data} = $file->slurp;
$c->stash->{devfw}->update($form->params);
$c->stash->{devfw}->update($form->values);
delete $c->session->{created_objects}->{device};
});
NGCP::Panel::Utils::Message->info(
@ -707,8 +731,8 @@ sub devconf_create :Chained('base') :PathPart('config/create') :Args(0) :Does(AC
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
my $devmod = $c->stash->{devmod_rs}->find($form->params->{device}{id},{'+columns' => [qw/mac_image front_image/]});
my $devconf = $devmod->create_related('autoprov_configs', $form->params);
my $devmod = $c->stash->{devmod_rs}->find($form->values->{device}{id},{'+columns' => [qw/mac_image front_image/]});
my $devconf = $devmod->create_related('autoprov_configs', $form->values);
delete $c->session->{created_objects}->{device};
$c->session->{created_objects}->{config} = { id => $devconf->id };
});
@ -804,11 +828,11 @@ sub devconf_edit :Chained('devconf_base') :PathPart('edit') :Args(0) {
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
$form->params->{device_id} = $form->params->{device}{id};
delete $form->params->{device};
$form->values->{device_id} = $form->values->{device}{id};
delete $form->values->{device};
use Data::Printer; p $form->params;
$c->stash->{devconf}->update($form->params);
use Data::Printer; p $form->values;
$c->stash->{devconf}->update($form->values);
delete $c->session->{created_objects}->{device};
});
NGCP::Panel::Utils::Message->info(
@ -874,10 +898,10 @@ sub devprof_create :Chained('base') :PathPart('profile/create') :Args(0) :Does(A
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
$form->params->{config_id} = $form->params->{config}{id};
delete $form->params->{config};
$form->values->{config_id} = $form->values->{config}{id};
delete $form->values->{config};
$c->model('DB')->resultset('autoprov_profiles')->create($form->params);
$c->model('DB')->resultset('autoprov_profiles')->create($form->values);
delete $c->session->{created_objects}->{config};
});
@ -924,6 +948,45 @@ sub devprof_base :Chained('base') :PathPart('profile') :CaptureArgs(1) :Does(ACL
}
}
sub devprof_extensions :Chained('devprof_base') :PathPart('extensions') :Args(0):Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) :AllowedRole(subscriberadmin) {
my ($self, $c) = @_;
my $rs = $c->stash->{devprof}->config->device->autoprov_extensions_link;
my $device_info = { $c->stash->{devprof}->config->device->get_inflated_columns };
foreach(qw/front_image mac_image/){
delete $device_info->{$_};
}
my $data = {
'device' => $device_info,
'profile' => { $c->stash->{devprof}->get_inflated_columns},
'extensions' => { map {
$_->extension->id => {
$_->extension->get_inflated_columns,
'ranges' => [
map {
$_->get_inflated_columns,
'annotations' => [
map {{
$_->get_inflated_columns,
}} $_->annotations->all,
],
} $_->extension->autoprov_device_line_ranges->all
],
}
} $rs->all },
};
$c->stash(
aaData => $data,
iTotalRecords => 1,
iTotalDisplayRecords => 1,
sEcho => int($c->request->params->{sEcho} // 1),
);
$c->detach( $c->view("JSON") );
}
sub devprof_get_lines :Chained('devprof_base') :PathPart('lines/ajax') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) :AllowedRole(subscriberadmin) {
my ($self, $c) = @_;
@ -940,29 +1003,56 @@ sub devprof_get_lines :Chained('devprof_base') :PathPart('lines/ajax') :Args(0)
$c->detach( $c->view("JSON") );
}
sub devprof_get_annotated_lines :Chained('devprof_base') :PathPart('annolines/ajax') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) :AllowedRole(subscriberadmin) {
sub devprof_get_annotated_info :Chained('devprof_base') :PathPart('annolines/ajax') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) :AllowedRole(subscriberadmin) {
my ($self, $c) = @_;
$self->get_annotated_info($c, $c->stash->{devprof}->config->device );
}
sub devmod_get_annotated_info :Chained('devmod_base') :PathPart('annolines/ajax') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) :AllowedRole(subscriberadmin) {
my ($self, $c) = @_;
$self->get_annotated_info($c, $c->stash->{devmod} );
}
my $rs = $c->stash->{devprof}->config->device->autoprov_device_line_ranges;
my @ranges = map {{
$_->get_inflated_columns,
annotations => [
map {{
sub get_annotated_info :Privat {
my ($self, $c, $devmod) = @_;
my $device_info = { $devmod->get_inflated_columns };
foreach(qw/front_image mac_image/){
delete $device_info->{$_};
}
my $gather_ranges_info = sub {
my $rs = shift;
return [
{ map {
$_->get_inflated_columns,
}} $_->annotations->all,
],
}} $rs->all;
$c->stash(aaData => \@ranges,
iTotalRecords => scalar @ranges,
iTotalDisplayRecords => scalar @ranges,
sEcho => int($c->request->params->{sEcho} // 1),
'annotations' => [
map {{
$_->get_inflated_columns,
}} $_->annotations->all,
],
} $rs->all }
];
};
my $data = {
'device' => $device_info,
'ranges' => $gather_ranges_info->( $devmod->autoprov_device_line_ranges ),
'extensions' => { map {
$_->extension->id => {
$_->extension->get_inflated_columns,
'ranges' => $gather_ranges_info->( $_->extension->autoprov_device_line_ranges ),
}
} $devmod->autoprov_extensions_link->all },
};
$c->stash(
aaData => $data,
iTotalRecords => 1,
iTotalDisplayRecords => 1,
sEcho => int($c->request->params->{sEcho} // 1),
);
$c->detach( $c->view("JSON") );
}
sub devprof_delete :Chained('devprof_base') :PathPart('delete') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
@ -1012,10 +1102,10 @@ sub devprof_edit :Chained('devprof_base') :PathPart('edit') :Args(0) :Does(ACL)
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
$form->params->{config_id} = $form->params->{config}{id};
delete $form->params->{config};
$form->values->{config_id} = $form->values->{config}{id};
delete $form->values->{config};
$c->stash->{devprof}->update($form->params);
$c->stash->{devprof}->update($form->values);
delete $c->session->{created_objects}->{config};
});

@ -38,6 +38,7 @@ sub render_element {
table_titles => $self->table_titles,
errors => $self->errors,
language_file => $self->language_file,
wrapper_class => ref $self->wrapper_class eq 'ARRAY' ? join (' ', @{$self->wrapper_class}) : $self->wrapper_class,
};
my $t = new Template({

@ -69,10 +69,15 @@ has_field 'line.line' => (
type => 'Hidden',
required => 1,
);
has_field 'line.extension_unit' => (
type => 'Hidden',
#we allow also other for of the unit specification, or even no unit - then db value will be 0
required => 0,
);
sub validate_line_line {
my ($self, $field) = @_;
$field->clear_errors;
unless($field->value =~ /^\d+\.\d+$/) {
unless($field->value =~ /^\d+\.\d+(?:\.\d+)?$/) {
my $err_msg = 'Invalid line value';
$field->add_error($err_msg);
}

@ -24,6 +24,27 @@ has_field 'vendor' => (
javascript => ' onchange="vendor2bootstrapMethod(this);" ',
},
);
has_field 'type' => (
type => 'Select',
required => 1,
label => 'Model type',
options => [
{ label => 'Phone device', value => 'phone' },
{ label => 'Extension device', value => 'extension' },
],
default => 'phone',
element_attr => {
rel => ['tooltip'],
title => ['Phone or the phone extension'],
javascript => ' onchange="typeDynamicFields(this.options[this.selectedIndex].value);" ',
},
);
has_field 'extensions_num' => (
type => 'Integer',
label => 'Max extensions number',
default => '0',
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone/],
);
has_field 'model' => (
type => 'Text',
@ -49,6 +70,20 @@ has_field 'mac_image' => (
max_size => '67108864', # 64MB
);
has_field 'connectable_models' => (
type => '+NGCP::Panel::Field::DataTable',
label => 'Connectable Models',
do_label => 0,
do_wrapper => 1,
required => 0,
#wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-extension/],
template => 'helpers/datatables_multifield.tt',
ajax_src => '/device/model/ajax',
table_titles => ['#', 'Type', 'Vendor', 'Model'],
table_fields => ['id', 'type', 'vendor', 'model'],
);
has_field 'linerange' => (
type => 'Repeatable',
label => 'Line/Key Range',
@ -201,6 +236,7 @@ has_field 'bootstrap_method' => (
type => 'Select',
required => 1,
label => 'Bootstrap Method',
wrapper_class => [qw/ngcp-type-phone ngcp-devicetype ngcp-devicetype-phone/],
options => [
{ label => 'Cisco', value => 'http' },
{ label => 'Panasonic', value => 'redirect_panasonic' },
@ -211,7 +247,6 @@ has_field 'bootstrap_method' => (
element_attr => {
rel => ['tooltip'],
title => ['Method to configure the provisioning server on the phone. One of http, redirect_panasonic, redirect_yealink, redirect_polycom.'],
# TODO: ????
javascript => ' onchange="bootstrapDynamicFields(this.options[this.selectedIndex].value);" ',
},
);
@ -220,6 +255,8 @@ has_field 'bootstrap_uri' => (
required => 0,
label => 'Bootstrap URI',
default => '',
#we don't show this field for polycom, because polycom doesn't support now any possibility to configure provisioning url via API
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-http ngcp-bootstrap-config-redirect_panasonic ngcp-bootstrap-config-redirect_yealink/],
element_attr => {
rel => ['tooltip'],
title => ['Custom provisioning server URI.'],
@ -232,7 +269,7 @@ has_field 'bootstrap_config_http_sync_uri' => (
required => 0,
label => 'Bootstrap Sync URI',
default => 'http://[% client.ip %]/admin/resync',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-http/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-http/],
element_attr => {
rel => ['tooltip'],
title => ['The sync URI to set the provisioning server of the device (e.g. http://client.ip/admin/resync. The client.ip variable is automatically expanded during provisioning time.'],
@ -248,7 +285,7 @@ has_field 'bootstrap_config_http_sync_method' => (
{ label => 'POST', value => 'POST' },
],
default => 'GET',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-http/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-http/],
element_attr => {
rel => ['tooltip'],
title => ['The HTTP method to set the provisioning server (one of GET, POST).'],
@ -260,7 +297,7 @@ has_field 'bootstrap_config_http_sync_params' => (
required => 0,
label => 'Bootstrap Sync Parameters',
default => '[% server.uri %]/$MA',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-http/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-http/],
element_attr => {
rel => ['tooltip'],
title => ['The parameters appended to the sync URI when setting the provisioning server, e.g. server.uri/$MA. The server.uri variable is automatically expanded during provisioning time.'],
@ -271,7 +308,7 @@ has_field 'bootstrap_config_redirect_panasonic_user' => (
required => 0,
label => 'Panasonic username',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_panasonic/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_panasonic/],
element_attr => {
rel => ['tooltip'],
title => ['Username used to configure bootstrap url on Panasonic redirect server. Obtained from Panasonic.'],
@ -282,7 +319,7 @@ has_field 'bootstrap_config_redirect_panasonic_password' => (
required => 0,
label => 'Panasonic password',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_panasonic/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_panasonic/],
element_attr => {
rel => ['tooltip'],
title => ['Password used to configure bootstrap url on Panasonic redirect server. Obtained from Panasonic.'],
@ -293,7 +330,7 @@ has_field 'bootstrap_config_redirect_yealink_user' => (
required => 0,
label => 'Yealink username',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_yealink/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_yealink/],
element_attr => {
rel => ['tooltip'],
title => ['Username used to configure bootstrap url on Yealink redirect server. Obtained from Yealink.'],
@ -304,7 +341,7 @@ has_field 'bootstrap_config_redirect_yealink_password' => (
required => 0,
label => 'Yealink password',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_yealink/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_yealink/],
element_attr => {
rel => ['tooltip'],
title => ['Password used to configure bootstrap url on Yealink redirect server. Obtained from Yealink.'],
@ -315,7 +352,7 @@ has_field 'bootstrap_config_redirect_polycom_user' => (
required => 0,
label => 'Polycom username',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_polycom/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_polycom/],
element_attr => {
rel => ['tooltip'],
title => ['Username used to configure bootstrap url on Polycom redirect server.'],
@ -326,7 +363,7 @@ has_field 'bootstrap_config_redirect_polycom_password' => (
required => 0,
label => 'Polycom password',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_polycom/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_polycom/],
element_attr => {
rel => ['tooltip'],
title => ['Password used to configure bootstrap url on Polycom redirect server.'],
@ -337,7 +374,7 @@ has_field 'bootstrap_config_redirect_polycom_profile' => (
required => 0,
label => 'Polycom profile',
default => '',
wrapper_class => [qw/ngcp-bootstrap-config ngcp-bootstrap-config-redirect_polycom/],
wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_polycom/],
element_attr => {
rel => ['tooltip'],
title => ['Preliminary created in ZeroTouch Provisioning console Polycom ZTP profile. Refer to documentation.'],
@ -354,7 +391,7 @@ has_field 'save' => (
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/vendor model linerange linerange_add bootstrap_uri bootstrap_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile front_image mac_image/],
render_list => [qw/vendor model type extensions_num linerange linerange_add bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile connectable_models front_image mac_image/],
);
has_block 'actions' => (

@ -7,7 +7,7 @@ use Moose::Util::TypeConstraints;
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/reseller vendor model linerange bootstrap_uri bootstrap_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile/],
render_list => [qw/reseller vendor model type extensions_num linerange bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile connectable_models/],
);
override 'field_list' => sub {

@ -26,7 +26,7 @@ has_field 'save' => (
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/reseller vendor model linerange linerange_add bootstrap_uri bootstrap_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile front_image mac_image/],
render_list => [qw/reseller vendor model type extensions_num linerange linerange_add bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile connectable_models front_image mac_image/],
);
has_block 'actions' => (

@ -16,9 +16,11 @@ use File::Type;
use Data::Dumper;
use NGCP::Panel::Form::Device::ModelAPI;
use NGCP::Panel::Utils::DeviceBootstrap;
use NGCP::Panel::Utils::Device;
sub get_form {
my ($self, $c) = @_;
#use_fields_for_input_without_param
return NGCP::Panel::Form::Device::ModelAPI->new(ctx => $c);
}
@ -63,29 +65,28 @@ sub resource_from_item {
run => 0,
);
$resource{reseller_id} = int($item->reseller_id);
$resource{id} = int($item->id);
$resource{linerange} = [];
foreach my $field (qw/reseller_id id extensions_num/){
$resource{$field} = int($item->$field // 0);
}
foreach my $field (qw/linerange connectable_models/){
$resource{$field} = [];
}
foreach my $range($item->autoprov_device_line_ranges->all) {
my $r = { $range->get_inflated_columns };
foreach my $f(qw/device_id num_lines/) {
delete $r->{$f};
}
$r->{id} = int($r->{id});
foreach my $f(qw/can_private can_shared can_blf/) {
$r->{$f} = $r->{$f} ? JSON::true : JSON::false;
}
$r->{keys} = [];
foreach my $key($range->annotations->all) {
push @{ $r->{keys} }, {
x => int($key->x),
y => int($key->y),
labelpos => $key->position,
};
$self->process_range( \%resource, $range );
}
if('extension' eq $item->type){
# show possible devices for extension
$resource{connectable_models} = [map {$_->device->id} ($item->autoprov_extension_device_link->all) ];
}else{
# we don't need show possible extensions - we will show their ranges
# add ranges of the possible extensions
foreach my $extension_link ($item->autoprov_extensions_link->all){
my $extension = $extension_link->extension;
push @{$resource{connectable_models}}, $extension->id;
foreach my $range($extension->autoprov_device_line_ranges->all) {
$self->process_range( \%resource, $range, sub { my $r = shift; $r->{extension_range} = $extension->id;} );#
}
}
$r->{num_lines} = @{ $r->{keys} };
push @{ $resource{linerange} }, $r;
}
return \%resource;
}
@ -120,7 +121,7 @@ sub update_item {
my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id});
unless($reseller) {
$c->log->error("invalid reseller_id '$$resource{reseller_id}', does not exist");
$c->log->error("invalid reseller_id '".((defined $resource->{reseller_id})?$resource->{reseller_id} : "undefined")."', does not exist");
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid reseller_id, does not exist");
return;
}
@ -156,7 +157,7 @@ sub update_item {
$resource->{mac_image} = $front_image->slurp;
$resource->{mac_image_type} = $ft->mime_type($resource->{mac_image});
}
my $connectable_models = delete $resource->{connectable_models};
my $sync_parameters = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_prefetch($c, $item, $resource);
my $credentials = NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_prefetch($c, $item, $resource);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_clear($c, $resource);
@ -167,7 +168,7 @@ sub update_item {
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_credentials_store($c, $item, $credentials);
NGCP::Panel::Utils::DeviceBootstrap::devmod_sync_parameters_store($c, $item, $sync_parameters);
NGCP::Panel::Utils::DeviceBootstrap::dispatch_devmod($c, 'register_model', $item);
NGCP::Panel::Utils::Device::process_connectable_models($c, 0, $item, $connectable_models );
my @existing_range = ();
my $range_rs = $item->autoprov_device_line_ranges;
foreach my $range(@{ $linerange }) {
@ -187,17 +188,28 @@ sub update_item {
unless(ref $range->{keys} eq "ARRAY") {
$c->log->error("linerange.keys must be array");
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid linerange.keys parameter, must be array");
#last? not next?
last;
}
if(defined $range->{id}) {
my $range_by_id = $c->model('DB')->resultset('autoprov_device_line_ranges')->find($range->{id});
if( $range_by_id && ( $range_by_id->device_id != $item->id ) ){
#this is extension linerange, stop processing this linerange completely
#we should care about it here due to backward compatibility, so API user still can make GET => PUT without excluding extension ranges
next;
}
}
#/check input section end
$range->{num_lines} = @{ $range->{keys} }; # backward compatibility
my $keys = delete $range->{keys};
my $old_range;
if(defined $range->{id}) {
# should be an existing range, do update
$old_range = $range_rs->find($range->{id});
delete $range->{id};
unless($old_range) {
unless($old_range) {#really this is strange situation
delete $range->{id};
$old_range = $range_rs->create($range);
} else {
# formhandler only passes set check-boxes, so explicitely unset here
@ -244,9 +256,31 @@ sub update_item {
$range_rs->search({
id => { 'not in' => \@existing_range },
})->delete_all;
return $item;
}
sub process_range {
my($self, $resource, $range, $process_range_cb ) = @_;
my $r = { $range->get_inflated_columns };
foreach my $f(qw/device_id num_lines/) {
delete $r->{$f};
}
$r->{id} = int($r->{id});
foreach my $f(qw/can_private can_shared can_blf/) {
$r->{$f} = $r->{$f} ? JSON::true : JSON::false;
}
$r->{keys} = [];
foreach my $key($range->annotations->all) {
push @{ $r->{keys} }, {
x => int($key->x),
y => int($key->y),
labelpos => $key->position,
};
}
$r->{num_lines} = @{ $r->{keys} };
( ( defined $process_range_cb ) && ( 'CODE' eq ref $process_range_cb ) ) and $process_range_cb->($r);
push @{ $resource->{linerange} }, $r;
}
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,68 @@
package NGCP::Panel::Utils::Device;
use strict;
sub process_connectable_models{
my ($c, $just_created, $devmod, $connectable_models) = @_;
my $schema = $c->model('DB');
if($connectable_models){
my @columns = ('device_id' , 'extension_id');
if('extension' eq $devmod->type){
#extension can be connected to other extensions? If I remember right - yes.
@columns = reverse @columns;
}else{
#we defenitely can't connect phone to phone
my $phone2phone = $schema->resultset('autoprov_devices')->search_rs({
'type' => 'phone',
'id' => { 'in' => $connectable_models },
});
if($phone2phone->first){
die("Phone can't be connected to the phone as extension.");
}
}
if(!$just_created){
#we don't need to clear old relations, because we just created this device
$schema->resultset('autoprov_device_extensions')->search_rs({
$columns[0] => $devmod->id,
})->delete;
}
foreach my $connected_id(@$connectable_models){
if($devmod->id == $connected_id){
die("Device can't be connected to itself as extension.");
}
$schema->resultset('autoprov_device_extensions')->create({
$columns[0] => $devmod->id,
$columns[1] => $connected_id,
});
}
}
}
1;
=head1 NAME
NGCP::Panel::Utils::Device
=head1 DESCRIPTION
Diffrent business logic method for pbx devices
=head1 METHODS
=head2 process_connectable_models
Process data tolink devices and extensions in the DB.
=head1 AUTHOR
Irina Peshinskaya C<< <ipeshinskaya@sipwise.com> >>
=head1 LICENSE
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
# vim: set tabstop=4 expandtab:

@ -561,7 +561,7 @@ sub create_all_sipsip {
my $ind = " " x ($self->classinfo->{$name}{indent}*4);
printf "%-37s: %d\n", "$ind$name", $resp->code;
if ($resp->code != 200) {
use DDP; p $resp->data;
#use DDP; p $resp->data;
}
}
}

@ -0,0 +1,586 @@
package NGCP::Panel::Utils::Test::Collection;
use strict;
use Test::More;
use Moose;
use JSON;
use LWP::UserAgent;
use HTTP::Request::Common;
use Net::Domain qw(hostfqdn);
use URI;
use Clone qw/clone/;
use Data::Dumper;
has 'ua' => (
is => 'rw',
isa => 'LWP::UserAgent',
builder => '_init_ua',
);
has 'base_uri' => (
is => 'ro',
isa => 'Str',
default => 'https://192.168.56.7:1444' || $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443'),
);
has 'name' => (
is => 'rw',
isa => 'Str',
);
has 'embedded_resources' => (
is => 'rw',
isa => 'ArrayRef',
default => sub { [] },
);
has 'methods' => (
is => 'rw',
isa => 'HashRef',
default => sub { {
'collection' =>{
'all' => {map {$_ => 1} qw/GET HEAD OPTIONS POST/},
'allowed' => {map {$_ => 1} qw/GET HEAD OPTIONS POST/}, #some default
},
'item' =>{
'all' => {map {$_ => 1} qw/GET HEAD OPTIONS PUT PATCH POST DELETE/},
'allowed' => {map {$_ => 1} qw/GET HEAD OPTIONS PUT PATCH DELETE/}, #some default
},
} },
);
has 'content_type' => (
is => 'rw',
isa => 'HashRef',
default => sub {{
POST => 'application/json',
PUT => 'application/json',
PATCH => 'application/json-patch+json',
}},
);
#state variables - smth like predefined stash
has 'DATA_ITEM' => (
is => 'rw',
isa => 'Ref',
);
has 'DATA_ITEM_STORE' => (
is => 'rw',
isa => 'Ref',
);
after 'DATA_ITEM_STORE' => sub {
my $self = shift;
if(@_){
#$self->DATA_ITEM($self->DATA_ITEM_STORE);
$self->form_data_item;
}
};
has 'DATA_CREATED' => (
is => 'rw',
isa => 'HashRef',
builder => 'clear_data_created',
);
has 'URI_CUSTOM' =>(
is => 'rw',
isa => 'Str',
);
has 'URI_CUSTOM_STORE' =>(
is => 'rw',
isa => 'Str',
);
before 'URI_CUSTOM' => sub {
my $self = shift;
if(@_){
if($self->URI_CUSTOM_STORE){
die('Attempt to set custom uri second time without restore. Custom uri is not a stack. Clear or restore it first, please.');
}else{
$self->URI_CUSTOM_STORE($self->URI_CUSTOM);
}
}
};
has 'ENCODE_CONTENT' => (
is => 'rw',
isa => 'Str',
default => 'json',
);
sub _init_ua {
my $self = shift;
my $valid_ssl_client_cert = $ENV{API_SSL_CLIENT_CERT} ||
"/etc/ngcp-panel/api_ssl/NGCP-API-client-certificate.pem";
my $valid_ssl_client_key = $ENV{API_SSL_CLIENT_KEY} ||
$valid_ssl_client_cert;
my $ssl_ca_cert = $ENV{ API_SSL_CA_CERT} || "/etc/ngcp-panel/api_ssl/api_ca.crt";
my $ua = LWP::UserAgent->new;
$ua->ssl_opts(
SSL_cert_file => $valid_ssl_client_cert,
SSL_key_file => $valid_ssl_client_key,
SSL_ca_file => $ssl_ca_cert,
);
#$ua->credentials( $self->base_uri, '', 'administrator', 'administrator' );
#$ua->ssl_opts(
# verify_hostname => 0,
# SSL_verify_mode => 0x00,
#);
return $ua;
};
sub clear_data_created{
my($self) = @_;
$self->DATA_CREATED({
ALL => {},
FIRST => undef,
});
return $self->DATA_CREATED;
}
sub form_data_item{
my($self, $data_cb, $data_cb_data) = @_;
$self->{DATA_ITEM} ||= clone($self->DATA_ITEM_STORE);
(defined $data_cb) and $data_cb->($self->DATA_ITEM,$data_cb_data);
return $self->DATA_ITEM;
}
sub get_hal_name{
my($self) = @_;
return "ngcp:".$self->name;
}
sub restore_uri_custom{
my($self) = @_;
$self->URI_CUSTOM($self->URI_CUSTOM_STORE);
$self->URI_CUSTOM_STORE(undef);
}
sub get_uri_collection{
my($self) = @_;
return $self->base_uri."/api/".$self->name.($self->name ? "/" : "");
}
sub get_uri_firstitem{
my($self) = @_;
if(!$self->DATA_CREATED->{FIRST}){
my($res,$list_collection,$req) = $self->check_item_get($self->get_uri_collection."?page=1&rows=1");
my $hal_name = $self->get_hal_name;
if(ref $list_collection->{_links}->{$hal_name} eq "HASH") {
$self->DATA_CREATED->{FIRST} = $list_collection->{_links}->{$hal_name}->{href};
} else {
$self->DATA_CREATED->{FIRST} = $list_collection->{_embedded}->{$hal_name}->[0]->{_links}->{self}->{href};
}
}
$self->DATA_CREATED->{FIRST} //= '';
return $self->base_uri.'/'.$self->DATA_CREATED->{FIRST};
}
sub get_uri_current{
my($self) = @_;
$self->URI_CUSTOM and return $self->URI_CUSTOM;
return $self->get_uri_firstitem;
}
sub encode_content{
my($self,$content, $type) = @_;
$type //= $self->ENCODE_CONTENT;
my %json_types = (
'application/json' => 1,
'application/json-patch+json' => 1,
'json' => 1,
);
#print "content=$content;\n\n";
if($content){
if( $json_types{$type} && (('HASH' eq ref $content) ||('ARRAY' eq ref $content)) ){
return JSON::to_json($content);
}
}
return $content;
}
sub request{
my($self,$req) = @_;
#print $req->as_string;
$self->ua->request($req);
}
sub get_request_put{
my($self,$content,$uri) = @_;
$uri ||= $self->get_uri_current;
#This is for multipart/form-data cases
$content = $self->encode_content($content, $self->content_type->{PUT});
my $req = POST $uri,
Content_Type => $self->content_type->{POST},
$content ? ( Content => $content ) : ();
$req->method('PUT');
$req->header('Prefer' => 'return=representation');
return $req;
}
sub get_request_patch{
my($self,$uri) = @_;
$uri ||= $self->get_uri_current;
my $req = HTTP::Request->new('PATCH', $uri);
$req->header('Prefer' => 'return=representation');
$req->header('Content-Type' => $self->content_type->{PATCH} );
return $req;
}
sub request_put{
my($self,$content,$uri) = @_;
$uri ||= $self->get_uri_current;
my $req = $self->get_request_put( $content, $uri );
my $res = $self->request($req);
#print Dumper $res;
my $err = $res->decoded_content ? JSON::from_json($res->decoded_content) : '';
return wantarray ? ($res,$err,$req) : $res;
}
sub request_patch{
my($self,$content,$uri, $req) = @_;
$uri ||= $self->get_uri_current;
$req ||= $self->get_request_patch($uri);
#patch is always a json
$content = $self->encode_content($content, $self->content_type->{PATCH});
$content and $req->content($content);
my $res = $self->request($req);
my $err = $res->decoded_content ? JSON::from_json($res->decoded_content) : '';
#print Dumper [$res,$err,$req];
return ($res,$err,$req);
}
sub request_post{
my($self, $data_cb, $data_in, $data_cb_data) = @_;
my $data = $data_in || clone($self->DATA_ITEM);
defined $data_cb and $data_cb->($data, $data_cb_data);
my $content = {
$data->{json} ? ( json => JSON::to_json(delete $data->{json}) ) : (),
%$data,
};
$content = $self->encode_content($content, $self->content_type->{POST} );
#form-data is set automatically, despite on $self->content_type->{POST}
my $req = POST $self->get_uri_collection,
Content_Type => $self->content_type->{POST},
Content => $content;
my $res = $self->request($req);
my $err = $res->decoded_content ? JSON::from_json($res->decoded_content) : '';
return ($res,$err,$req);
};
sub request_options{
my ($self,$uri) = @_;
# OPTIONS tests
$uri ||= $self->get_uri_current;
my $req = HTTP::Request->new('OPTIONS', $uri);
my $res = $self->request($req);
my $content = $res->decoded_content ? JSON::from_json($res->decoded_content) : '';
return($req,$res,$content);
}
sub request_delete{
my ($self,$uri) = @_;
# DELETE tests
#no auto rows for deletion
my $req = HTTP::Request->new('DELETE', $uri);
my $res = $self->request($req);
my $content = $res->decoded_content ? JSON::from_json($res->decoded_content) : '';
return($req,$res,$content);
}
sub check_options_collection{
my ($self) = @_;
# OPTIONS tests
my $req = HTTP::Request->new('OPTIONS', $self->get_uri_collection );
my $res = $self->request($req);
is($res->header('Accept-Post'), "application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-".$self->name, "check Accept-Post header in options response");
$self->check_methods($res,'collection');
}
sub check_options_item{
my ($self,$uri) = @_;
# OPTIONS tests
$uri ||= $self->get_uri_current;
my $req = HTTP::Request->new('OPTIONS', $uri);
my $res = $self->request($req);
$self->check_methods($res,'item');
}
sub check_methods{
my($self, $res, $area) = @_;
is($res->code, 200, "check $area options request");
my $opts = JSON::from_json($res->decoded_content);
my @hopts = split /\s*,\s*/, $res->header('Allow');
ok(exists $opts->{methods} && ref $opts->{methods} eq "ARRAY", "check for valid 'methods' in body");
foreach my $opt(keys %{$self->methods->{$area}->{all}} ) {
if(exists $self->methods->{$area}->{allowed}->{$opt}){
ok(grep(/^$opt$/, @hopts), "check for existence of '$opt' in Allow header");
ok(grep(/^$opt$/, @{ $opts->{methods} }), "check for existence of '$opt' in body");
}else{
ok(!grep(/^$opt$/, @hopts), "check for absence of '$opt' in Allow header");
ok(!grep(/^$opt$/, @{ $opts->{methods} }), "check for absence of '$opt' in body");
}
}
}
sub check_create_correct{
my($self, $number, $uniquizer_cb, $keep_data) = @_;
if(!$keep_data){
$self->clear_data_created;
}
$self->DATA_CREATED->{ALL} //= {};
for(my $i = 1; $i <= $number; ++$i) {
my ($res, $err) = $self->request_post( $uniquizer_cb , undef, { i => $i} );
is($res->code, 201, "create test item $i");
my $location = $res->header('Location');
if($location){
$self->DATA_CREATED->{ALL}->{$location} = $i;
$self->DATA_CREATED->{FIRST} = $location unless $self->DATA_CREATED->{FIRST};
}
}
}
sub check_delete_use_created{
my($self,$uri) = @_;
my @uris = $uri ? ($uri) : keys $self->DATA_CREATED->{ALL};
foreach my $del_uri(@uris){
my($req,$res,$content) = $self->request_delete($self->base_uri.$del_uri);
is($res->code, 204, "check delete item $del_uri");
}
}
sub check_list_collection{
my($self, $check_embedded_cb) = @_;
my $nexturi = $self->get_uri_collection."?page=1&rows=5";
my @href = ();
do {
#print "nexturi=$nexturi;\n";
my $res = $self->ua->get($nexturi);
is($res->code, 200, "fetch collection page");
my $list_collection = JSON::from_json($res->decoded_content);
my $selfuri = $self->base_uri . $list_collection->{_links}->{self}->{href};
is($selfuri, $nexturi, "check _links.self.href of collection");
my $colluri = URI->new($selfuri);
ok($list_collection->{total_count} > 0, "check 'total_count' of collection");
my %q = $colluri->query_form;
ok(exists $q{page} && exists $q{rows}, "check existence of 'page' and 'row' in 'self'");
my $page = int($q{page});
my $rows = int($q{rows});
ok($rows != 0, "check existance of the 'rows'");
if($page == 1) {
ok(!exists $list_collection->{_links}->{prev}->{href}, "check absence of 'prev' on first page");
} else {
ok(exists $list_collection->{_links}->{prev}->{href}, "check existence of 'prev'");
}
if(($rows != 0) && ($list_collection->{total_count} / $rows) <= $page) {
ok(!exists $list_collection->{_links}->{next}->{href}, "check absence of 'next' on last page");
} else {
ok(exists $list_collection->{_links}->{next}->{href}, "check existence of 'next'");
}
if($list_collection->{_links}->{next}->{href}) {
$nexturi = $self->base_uri . $list_collection->{_links}->{next}->{href};
} else {
$nexturi = undef;
}
my $hal_name = $self->get_hal_name;
ok(((ref $list_collection->{_links}->{$hal_name} eq "ARRAY" ) ||
(ref $list_collection->{_links}->{$hal_name} eq "HASH" ) ), "check if 'ngcp:".$self->name."' is array/hash-ref");
my $check_embedded = sub {
my($embedded) = @_;
defined $check_embedded_cb and $check_embedded_cb->($embedded);
foreach my $embedded_name(@{$self->embedded_resources}){
ok(exists $embedded->{_links}->{'ngcp:'.$embedded_name}, "check presence of ngcp:$embedded_name relation");
}
};
# it is really strange - we check that the only element of the _links will be hash - and after this treat _embedded as hash too
#the only thing that saves us - we really will not get into the if ever
if(ref $list_collection->{_links}->{$hal_name} eq "HASH") {
$check_embedded->($list_collection->{_embedded}->{$hal_name});
push @href, $list_collection->{_links}->{$hal_name}->{href};
} else {
foreach my $item_c(@{ $list_collection->{_links}->{$hal_name} }) {
push @href, $item_c->{href};
}
foreach my $item_c(@{ $list_collection->{_embedded}->{$hal_name} }) {
# these relations are only there if we have zones/fees, which is not the case with an empty model
$check_embedded->($item_c);
push @href, $item_c->{_links}->{self}->{href};
}
}
} while($nexturi);
return \@href;
}
sub check_created_listed{
my($self,$listed) = @_;
my $created_items = clone($self->DATA_CREATED->{ALL});
$listed //= [];#to avoid error about not array reference
$created_items //= [];
foreach (@$listed){
delete $created_items->{$_};
}
is(scalar(keys %{$created_items}), 0, "check if all created test items have been foundin the list");
}
sub check_item_get{
my($self,$uri) = @_;
$uri ||= $self->get_uri_current;
my $req = HTTP::Request->new('GET', $uri);
my $res = $self->request($req);
is($res->code, 200, "fetch one item");
my $err = $res->decoded_content ? JSON::from_json($res->decoded_content) : '';
return wantarray ? ($res, $err, $req) : $res;
}
sub check_put_content_type_empty{
my($self) = @_;
# check if it fails without content type
my $req = $self->get_request_put;
$req->remove_header('Content-Type');
$req->remove_header('Prefer');
$req->header('Prefer' => "return=minimal");
my $res = $self->request($req);
is($res->code, 415, "check put missing content type");
}
sub check_put_content_type_wrong{
my($self) = @_;
# check if it fails with unsupported content type
my $req = $self->get_request_put;
$req->remove_header('Content-Type');
$req->header('Content-Type' => 'application/xxx');
my $res = $self->request($req);
is($res->code, 415, "check put invalid content type");
}
sub check_put_prefer_wrong{
my($self) = @_;
# check if it fails with invalid Prefer
my $req = $self->get_request_put;
$req->remove_header('Prefer');
$req->header('Prefer' => "return=invalid");
my $res = $self->request($req);
is($res->code, 400, "check put invalid prefer");
}
sub check_put_body_empty{
my($self) = @_;
# check if it fails with missing body
my $req = $self->get_request_put;
#$req->remove_header('Prefer');
#$req->header('Prefer' => "return=representation");
my $res = $self->request($req);
is($res->code, 400, "check put no body");
}
sub check_get2put{
my($self,$put_data_cb, $uri) = @_;
#$req->remove_header('Prefer');
#$req->header('Prefer' => "return=representation");
# PUT same result again
my ($get_res, $item_first_get, $get_req) = $self->check_item_get($uri);
my $item_first_put = clone($item_first_get);
delete $item_first_put->{_links};
delete $item_first_put->{_embedded};
# check if put is ok
(defined $put_data_cb) and $put_data_cb->($item_first_put);
my ($put_res,$item_put_result) = $self->request_put( $item_first_put, $uri );
is($put_res->code, 200, "check put successful");
is_deeply($item_first_get, $item_put_result, "check put if unmodified put returns the same");
}
sub check_put_bundle{
my($self) = @_;
$self->check_put_content_type_empty;
$self->check_put_content_type_wrong;
$self->check_put_prefer_wrong;
$self->check_put_body_empty;
}
sub check_patch_correct{
my($self,$content) = @_;
my ($res,$mod_model,$req) = $self->request_patch( $content );
is($res->code, 200, "check patched item");
is($mod_model->{_links}->{self}->{href}, $self->DATA_CREATED->{FIRST}, "check patched self link");
is($mod_model->{_links}->{collection}->{href}, '/api/'.$self->name.'/', "check patched collection link");
return ($res,$mod_model,$req);
}
sub check_patch_prefer_wrong{
my($self) = @_;
my $req = $self->get_request_patch;
$req->remove_header('Prefer');
$req->header('Prefer' => 'return=minimal');
my $res = $self->request($req);
is($res->code, 415, "check patch invalid prefer");
}
sub check_patch_content_type_empty{
my($self) = @_;
my $req = $self->get_request_patch;
$req->remove_header('Content-Type');
my $res = $self->request($req);
is($res->code, 415, "check patch missing media type");
}
sub check_patch_content_type_wrong{
my($self) = @_;
my $req = $self->get_request_patch;
$req->remove_header('Content-Type');
$req->header('Content-Type' => 'application/xxx');
my $res = $self->request($req);
is($res->code, 415, "check patch invalid media type");
}
sub check_patch_body_empty{
my($self) = @_;
my ($res,$content,$req) = $self->request_patch;
is($res->code, 400, "check patch missing body");
like($content->{message}, qr/is missing a message body/, "check patch missing body response");
}
sub check_patch_body_notarray{
my($self) = @_;
my ($res,$content,$req) = $self->request_patch(
{ foo => 'bar' },
);
is($res->code, 400, "check patch no array body");
like($content->{message}, qr/must be an array/, "check patch missing body response");
}
sub check_patch_op_missed{
my($self) = @_;
my ($res,$content,$req) = $self->request_patch(
[{ foo => 'bar' }],
);
is($res->code, 400, "check patch no op in body");
like($content->{message}, qr/must have an 'op' field/, "check patch no op in body response");
}
sub check_patch_op_wrong{
my($self) = @_;
my ($res,$content,$req) = $self->request_patch(
[{ op => 'bar' }],
);
is($res->code, 400, "check patch invalid op in body");
like($content->{message}, qr/Invalid PATCH op /, "check patch no op in body response");
}
sub check_patch_opreplace_paramsmiss{
my($self) = @_;
my ($res,$content,$req) = $self->request_patch(
[{ op => 'replace' }],
);
is($res->code, 400, "check patch missing fields for op");
like($content->{message}, qr/Missing PATCH keys /, "check patch missing fields for op response");
}
sub check_patch_opreplace_paramsextra{
my($self) = @_;
my ($res,$content,$req) = $self->request_patch(
[{ op => 'replace', path => '/foo', value => 'bar', invalid => 'sna' }],
);
is($res->code, 400, "check patch extra fields for op");
like($content->{message}, qr/Invalid PATCH key /, "check patch extra fields for op response");
}
sub check_patch_bundle{
my($self) = @_;
#$self->check_patch_prefer_wrong;
$self->check_patch_content_type_wrong;
$self->check_patch_content_type_empty;
$self->check_patch_body_empty;
$self->check_patch_body_notarray;
$self->check_patch_op_missed;
$self->check_patch_op_wrong;
$self->check_patch_opreplace_paramsmiss;
$self->check_patch_opreplace_paramsextra;
}
sub check_bundle{
my($self) = @_;
$self->check_options_collection;
# iterate over collection to check next/prev links and status
my $listed = $self->check_list_collection();
$self->check_created_listed($listed);
# test model item
$self->check_options_item;
$self->check_put_bundle;
$self->check_patch_bundle;
}
1;

@ -0,0 +1,311 @@
/**
* jQuery dump plugin. Inspect object properties in a popup window.
*
* Copyright (c) 2012 Block Alexander
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*/
(function ($) {
var __jqdump__dump_OFF = false; // global OFF switch
var position = { left: 100, top: 120 }; // popup window position
var defaultDepth = 2; // default dump depth
/**
* Dump object content in popup window with prettyprint and subnavigation
*
* <example>
* // dump navigator properties
* $.dump(window.navigator, 1, "window.navigator");
*
* // this may be used to disable all following function calls
* $.dump("off");
* </example>
*
* @param(obj): object to dump, or "off" string to disable all dump calls
* @param(iDepth): number, optional dumping depth, default 2
* @param(sHistoryPath): string, optional global properties path relative to the initial dump object value, used for display only.
* @returns: null
*/
$.dump = function (obj, iDepth, sHistoryPath) {
if (typeof(obj) == "string" && /^off$/i.test(obj)) { __jqdump__dump_OFF = true; }
return __jqdump__dump(obj, iDepth, sHistoryPath);
};
/**
* Dump object content in popup window with prettyprint and subnavigation
*
* <example>
* $("#element").dump(1);
* // same as
* $.dump($("#element"), 1, "(#element)");
* </example>
*
* Call type: window.dump/$.dump( ... )
* @param(iDepth): number, optional dumping depth, default 2
* @returns: null
*/
$.fn.dump = function (iDepth) {
return __jqdump__dump(this, iDepth, this.selector? "("+ this.selector +")" : "");
};
/**
* Dump object content in popup window with prettyprint and subnavigation
*
* @param(obj): object to dump
* @param(iDepth): number, optional dumping depth, default 2
* @param(sHistoryPath): string, optional global properties path relative to the initial dump object value, used for display only.
* @returns: null
*/
function __jqdump__dump (obj, iDepth, sHistoryPath) {
if (__jqdump__dump_OFF) { return null; }
// store current object value to allow continious/recursive dump browsing via window.opener.__jqdump__
__jqdump__ = {data: obj, dump: __jqdump__dump };
// provide defaults as needed
iDepth = (typeof(iDepth) == "number" && iDepth > 0? parseInt(iDepth, 10) : defaultDepth);
sHistoryPath = (typeof(sHistoryPath) == "string" && sHistoryPath.length > 0? sHistoryPath : "OBJECT");
// adjust new window position
position = { top: (position.top - 30) % 120, left: (position.left - 10) % 100 };
// try to popup blank page
var dumpWindow = window.open("about:blank", "_blank"
, "width=600,height=800,menubar=0,left="+ position.left +",top="+ position.top
+",location=0,toolbar=0,status=0,scrollbars=1,resizable=1", true);
// popup blocked?
if (!dumpWindow || dumpWindow.closed == true) {
if (confirm("Dump Window couldn't showup cause of popup blocker.\nContinue using current window?")) {
dumpWindow = window;
} else {
return null;
}
}
// fill the page with dump content
dumpWindow.document.write("<html><head><title>"+ sHistoryPath +" @ "+ DateToHMSMString(new Date()) +"</title>"
+"<meta http-equiv='Content-Type' content='text/html;charset=utf-8'/>"
+"<style type='text/css'>"
+" body{background-color:#fff;color:#000;font-size:12px;margin-top:24px;}"
+" #toolbar{position:fixed;top:0px;right:0px;z-index:9999;}"
+" span.p.a:hover,span.p.a:hover+span {background-color: #B5F5FF;}"
+" a{text-decoration:none;}"
+" a:hover{text-decoration:underline;}"
+" .h{display:none;}" // hidden element
+" .a{cursor:pointer;}" // link like element
+" .s{color:#740;}" // string
+" .k{color:#427;font-weight:bold;font-style:italic;}" // key-word
+" .c{color:#666;font-style:italic;}" // comment
+" .u{color:#259;}" // reserved value
+" .p{color:#155;font-weight:bold;}" // punctuation
+" .d{color:#800;font-weight:bold;}" // diggit
+" .e{color:#900;font-style:italic;background-color:#FAA;}" // exception
+" .t{color:#080;font-weight:bold;}" // true boolean value
+" .f{color:#800;font-weight:bold;}" // false boolean value
+" .arg{color:#000;font-weight:normal;}" // number of function arguments
+"</style></head><body>"
+"<div id='toolbar'>"
+" <input type='button' id='btnClose' value='Close' onclick='window.close();'/>"
+" <input type='button' value='Collapse All' onclick='ToggleCollapse(true);'/>"
+" <input type='button' value='Expand All' onclick='ToggleCollapse(false);'/>"
+"</div>"
+"<code><pre>"+ sHistoryPath +" <span class='p'>=</span> "
+ __jqdump__next(obj, " ", iDepth, "__jqdump__.data", sHistoryPath)
+"<span class='p'>;</span></pre></code>"
// provide data and code to the parent window
+"<sc"+"ript type='text/javascript'><!-"+"-"
+"\n __jqdump__ = (window.opener? window.opener.__jqdump__ : window.__jqdump__);"
+"\n if (!__jqdump__) { __jqdump__ = {data: null, dump: function (o) { if (JSON) { alert(JSON.stringify(o)); } } }; }"
+"\n "
// focus the close button to allow fast window close by pressing button [space]
+"\n window.onload = function () {"
+"\n window.focus();"
+"\n document.getElementById('btnClose').focus();"
+"\n }"
+"\n "
+"\n function ToggleCollapse (bCollapse) {"
+"\n var span = document.getElementsByTagName('span');"
+"\n for (var i = 0; i < span.length; i++) {"
+"\n if (span[i].getAttribute('title') != 'collapse/expand' || (i == 1 && bCollapse)) { continue; }"
+"\n span[i].nextSibling.className = bCollapse? 'h' : '';"
+"\n }"
+"\n }"
+"\n-"+"-></sc"+"ript></body></html>"
);
// finalize writings
dumpWindow.document.close();
// ensure new window has data and code to continue further dumps
dumpWindow.__jqdump__ = {
data: __jqdump__.data
, dump: __jqdump__.dump
//*dbg*/, history: sHistoryPath
};
/**
* @param(obj): object to dump
* @param(sIndent): string, used for object properties alignment indentation
* @param(iDepth): number, dumping depth, defaults to 2
* @param(sContextPath): string, evaluable object properties path relative to the current obj value, used onclick event
* @param(sHistoryPath): string, global properties path relative to the initial dump obj value, used for display only
*/
function __jqdump__next (obj, sIndent, iDepth, sContextPath, sHistoryPath) {
var objType = typeof(obj);
if (null == obj && objType != "undefined") { return "<span class='u'>null</span>"; }
switch (objType) {
case "object": break;
case "undefined": return "<span class='u'>undefined</span>";
case "string": return obj.length? "\"<span class='s'>"+ htmlEscape(obj) +"</span>\"" : "\"\"";
case "number": return "<span class='d'>"+ obj.toString() +"</span>";
case "boolean": return "<span class='"+ (obj? "t" : "f") +"'>"+ obj.toString() +"</span>";
// allow dumping of function return value (simple function call without arguments)
case "function": return format(
"<a href='javascript:;' class='k'"
+" onclick='__jqdump__.dump((function(){try{return {0}();}catch(xcp){return {EXCEPTION_WRAPPER:xcp.toString()};}})()"
+","+ defaultDepth +",this.title);' title='{1}()'>func"+"tion({2})</a>"
, sContextPath
, sHistoryPath
, (obj.length? "<span class='arg'>"+ obj.length +"</span>" : "")
);
default: return "<span class='e'>/* Unknown object type: {"+ objType +"}*/</span>";
}
if (obj instanceof Date) { return "new Date(\""+ obj.toUTCString() +"\")"; }
if (iDepth == 0) { // stop here and allow deeper dumping by a click
return format("<a href='javascript:;' class='p' title='show more'"
+" onclick='__jqdump__.dump((function(){try{return {0};}catch(xcp){return {EXCEPTION_WRAPPER:xcp.toString()};}})(),"
+ defaultDepth +",\"{1}\");'"
+">{ ... }</a>"
, sContextPath
, sHistoryPath.replace(/"/g, "\\\""));
}
var bIsArray = (obj instanceof Array)
, sNewContextPath, sNewHistoryPath
, obja = [], prop;
var rv = [
"<span class='p a' title='collapse/expand'"
+" onclick='this.nextSibling.className=(this.nextSibling.className==\"h\"?\"\":\"h\")'"
+">&#160;"+ (bIsArray? "[" : "{") +"&#160;</span><span>\n"
];
try {
// making sorted array of object property names
if (bIsArray) {
for (var i = 0; i < obj.length; i++) { obja.push(i); }
} else {
for (prop in obj) { obja.push(prop); }
obja.sort(function (a, b) {
return (isNaN(a)? (a.toLowerCase() >= b.toLowerCase()? 1 : -1) : (Number(a) >= Number(b))? 1 : -1);
});
}
// quering object with names via sorted array
for (var c = 0, length = obja.length; c < length; c++) {
try {
prop = obja[c];
// skip self properties
if (/__jqdump__/.test(prop)) { continue; }
if (bIsArray) { // array index:
sNewContextPath = format("{0}[\"{1}\"]", sContextPath, prop);
sNewHistoryPath = format("{0}[\"{1}\"]", sHistoryPath, prop);
rv.push(format(
"{0}<span class='a c' onclick='alert(this.title);' title='{1}'>/*{2}*/</span> "
, sIndent, sNewHistoryPath, prop
));
} else {// object property:
if (/(\W)|(^\d)/.test(prop)) {//- as string
sNewContextPath = format("{0}[\"{1}\"]", sContextPath, prop);
sNewHistoryPath = format("{0}[\"{1}\"]", sHistoryPath, htmlEscape(prop));
rv.push(format(
"{0}<span class='s a' onclick='alert(this.title);' title='{1}'>\"{2}\"</span> <span class='p'>:</span> "
, sIndent, sNewHistoryPath, htmlEscape(prop)
));
} else {//- as conventional variable name
sNewContextPath = format("{0}.{1}", sContextPath, prop);
sNewHistoryPath = format("{0}.{1}", sHistoryPath, htmlEscape(prop));
rv.push(format(
"{0}<span class='a' onclick='alert(this.title);' title='{1}'>{2}</span> <span class='p'>:</span> "
, sIndent, sNewHistoryPath, htmlEscape(prop)
));
}
}
rv.push(__jqdump__next(obj[prop], sIndent +" ", iDepth - 1, sNewContextPath, sNewHistoryPath));
} catch (xcp) {
rv.push(format("<span class='e'>/*{0} - {1}*/</span>", xcp.name, xcp.message));
}
rv.push((c < (obja.length-1)? "<span class='p'>,</span>\n" : "\n"));
}
} catch (xcp) {
rv.push(format("<span class='e'>/*{0} - {1}*/</span>", xcp.name, xcp.message));
}
rv.push(format("{0}</span><span class='p'>{1}</span>", sIndent.replace(" ", ""), (bIsArray? "]" : "}")));
return rv.join("");
};
/**
* Escape native string characters before writing it as html text
*/
function htmlEscape (str) {
// trying to speedup the process by checking the length
return str.length? str.replace(/&/g, "&amp;").replace(/\</g, "&lt;").replace(/\>/g, "&gt;") : str;
};
/**
* Replaces each format item "{n}" in `this` string with the string equivalent of a corresponding
* parameters object's value. For example "{0}" reffers to the second argument, "{1}" to the third.
*
* @note: format identifier "{i}" may be repeated multiple times in any order as long as it reffers
* to corresponding position of the argument
* @param(0): first parameter is a format-string, containing meta information of insertion positions
* @param(...): any type, other arguments reffered by the format-string will be evaluated to string
* @return: string, formating result
*/
function format (/* ... */) {
var match = null, rv = arguments[0];
for (var c = 1, length = arguments.length; c < length; c++) {
match = new RegExp("\\{"+ (c - 1) +"\\}", "g");
if (match.test(rv)) {
rv = rv.replace(match, typeof(arguments[c]) == "undefined" ? "(undefined)" : arguments[c].toString());
}
}
match = null;
return rv;
};
/**
* Time to string in HH:MM:SS.mmm format
* @param(d): date object
*/
function DateToHMSMString (d) {
var iHours = d.getHours(), iMinutes = d.getMinutes(), iSeconds = d.getSeconds(), iMSeconds = d.getMilliseconds();
return (iHours < 10? "0" : "") + iHours +":"+ (iMinutes < 10? "0" : "") + iMinutes
+":"+ (iSeconds < 10? "0" : "") + iSeconds
+"."+ (iMSeconds < 100? "0" : "") + (iMSeconds < 10? "0" : "") + iMSeconds;
}
return null;
};
})(jQuery);

@ -11,7 +11,11 @@
$el.append('<div class="arrow-' + pos + '"></div>');
$el.addClass("annotate");
var linkPosition = $el.position();
//var linkPosition = $el.position();
var linkPosition = {
'top': $el.attr("data-pos-top"),
'left': $el.attr("data-pos-left")
};
var top, left;
switch(pos) {
case "top":

@ -13,6 +13,7 @@
{ level = 4, text = c.loc("global (including web login)") },
];
-%]
<script src="/js/jquery.dump.js"></script>
<div class="row">
<span>
@ -588,141 +589,293 @@
});
</script>
[% END -%]
[% IF c.config.features.cloudpbx && device_flag -%]
<style>
.annotated
{
position: relative;
margin: 20px 20px 0 20px;
}
.annotated img
{
display: block;
<style>
.annotated
{
position: relative;
margin: 20px 20px 0 20px;
}
.annotated img
{
display: block;
max-width: none;
}
.annotated a
{
text-decoration: none;
color: inherit;
}
.annotated .caption
{
white-space: nowrap;
}
.annotated select
{
height: 15px;
line-height: 15px;
padding: 0;
margin: 0;
font-size: 11px;
}
.annotated select.subselect
{
width: 180px;
}
.annotated select.modeselect
{
width: 60px;
}
</style>
<link rel="stylesheet" href="/css/ngcp-annotate.css">
<script src="/js/libs/ngcp-annotate.js"></script>
<link rel="stylesheet" type="text/css" href="/font/font-awesome/css/font-awesome.min.css"/>
<script>
var linekeys;
var fieldExtensions;
//can't be passed from events => global variable
var profileExtensionsData;
[% IF edit_flag == 1 -%]
if(!linekeys){
linekeys = {};
//keeps as keys modelDeviceId's for all extensions, mentioned in the field lineranges
//keys of the value keep all phone models linked to this extension
fieldExtensions = {};
[% FOR line IN pbx_device.autoprov_field_device_lines.all -%]
[%IF line.autoprov_device_line_range.device.type == 'extension'%]
[%extension_model_device_id = line.autoprov_device_line_range.device.id%]
if(!fieldExtensions['[%extension_model_device_id%]']){
fieldExtensions['[%extension_model_device_id%]']= {
'units': {},
'availablePhones': {}
};
}
fieldExtensions['[%extension_model_device_id%]']['units']['[%line.extension_unit%]'] = 1;
fieldExtensions['[%extension_model_device_id%]']['availablePhones'] = {
[% FOR link IN line.autoprov_device_line_range.device.autoprov_extension_device_link.all -%]
'[%link.device_id%]':1[%IF !loop.last %], [%END%]
[%END%]
};
[%END%]
linekeys['[% line.linerange_id %].[% line.key_num %].[% line.extension_unit %]'] = {
'mode': '[% line.line_type %]',
'sub': '[% line.provisioning_voip_subscriber.username %]@[% line.provisioning_voip_subscriber.domain.domain%]',
'subid': [% line.provisioning_voip_subscriber.id %],
'ext': '[% line.provisioning_voip_subscriber.pbx_extension%]'
};
[% END -%]
}
[% END -%]
function get_urls(in_data){
var data = {};
if((!in_data.type) || (in_data.type == undefined) || in_data.type == 'profile'){
in_data.type = 'profile';
if(!in_data.id){
in_data.id = $('div.controls #profile_id option:selected').first().attr('value');
}
.annotated a
{
text-decoration: none;
color: inherit;
if(!in_data.id) return;
data.uri_lines = '/device/profile/' + in_data.id + '/annolines/ajax';
data.uri_extensions = '/device/profile/' + in_data.id + '/extensions';
}
return data;
}
function create_ranges_annotations(id,type,ranges){
var divCnt = 1 + get_extension_divcnt(id);
var divId = 'annotated_'+type+(type === 'extension'?'_'+id+'_'+divCnt:'');
var uri_image = ( type == 'extension' ) ?'/device/model/' + id + '/frontimage' : '/device/profile/' + id + '/frontimage';
var markup = '<div class="annotated" id="'+divId+'">' +
'<img src="' +uri_image + '" />';
var linecmt = get_linecmt();
var unit = 0;
if(type === 'extension'){
//if we draw by button - it will fit without parameter
unit = get_extensions_num();
}
for(var i = 0; i < ranges.length; ++i) {
var range = ranges[i];
for(var j = 0; j < range.annotations.length; ++j) {
linecmt++;
$('#line\\.' + linecmt).remove();
var a = range.annotations[j];
var status = "unassigned";
var linekeyCurrent = 0;
[% IF edit_flag == 1 -%]
var idx = ranges[i].id+'.'+ a.line_index+'.'+ unit;
if( linekeys && linekeys[idx]){
linekeyCurrent = linekeys[idx];
status = "assigned";
}
[% END -%]
var action = '<i class="fa '+(linekeyCurrent ? 'fa-plus-square' : 'fa-user')+' fa-fw"></i> ' +
'<select class="subselect" name="line.' + linecmt + '.subscriber_id" id="line.' + linecmt + '.subscriber_id">' +
'<option value="0">'+(linekeyCurrent ? '[% c.loc("Subscriber") %]' : '[% c.loc("None") %]' )+'</option>' +
[% subs = [] -%]
[% FOR sub IN subs.merge(pbx_groups.all, subscribers.all) -%]
'<option value="[% sub.provisioning_voip_subscriber.id %]"'+((linekeyCurrent && linekeyCurrent.subid == '[% sub.provisioning_voip_subscriber.id %]')? ' selected="selected"' : '')+'>[% sub.provisioning_voip_subscriber.display_name ? sub.provisioning_voip_subscriber.display_name : sub.username %][% sub.provisioning_voip_subscriber.pbx_extension.defined ? " (" _ sub.provisioning_voip_subscriber.pbx_extension _ ")" : "" %]</option>' +
[% END -%]
'</select>' +
'<select class="modeselect" name="line.' + linecmt + '.type" id="line.' + linecmt + '.type">' +
[% FOR opt IN ["private", "shared", "blf"] -%]
(range.can_[% opt %] == "1" ?
'<option value="[% opt %]"'+((linekeyCurrent && linekeyCurrent.mode == '[% opt %]' ) ? ' selected="selected"' : '')+'>[% opt %]</option>' : '') +
[% END -%]
'</select>' +
'<input type="hidden" name="line.' + linecmt + '.line" id="line.' + linecmt + '.line" value="' + a.range_id + '.' + a.line_index + '.' + unit + '"/>' +
'<input type="hidden" name="line.' + linecmt + '.extension_unit" id="line.' + linecmt + '.extension_unit" value="' + unit + '"/>' +
'';
markup += '<div class="caption ' + status + '" style="top:' + a.y + 'px; left:' + a.x + 'px;" data-pos="' + a.position + '" data-pos-top="' + a.y + '" data-pos-left="' + a.x + '">' + action + '</div>';
}
.annotated .caption
{
white-space: nowrap;
}
if(type == 'extension'){
markup += '<div class="control-group delete_extension_button">'
+'<div class="controls">'
+'<div class="btn btn-primary pull-right rm_element btn" onclick="$(\'div#'+divId+'\').remove();">Remove Extension</div>'
+'</div>'
+'</div>';
}
markup += '</div>';
apply_markup(markup);
add_extension_button();
$("div.annotated").each(function(){
$(this).find("div.caption").annotate(this);
});
//return markup;
}
function apply_markup(markup){
var last = $('div.annotated').last();
if(!last || last.length < 1 ){
last =$('div.annotated');
}
if(!last || last.length < 1 ){
last = $('div.caption').last();
if(!last || last.length < 1 ){
last =$('div.caption');
}
.annotated select
{
height: 15px;
line-height: 15px;
padding: 0;
margin: 0;
font-size: 11px;
}
if(!last || last.length < 1 ){
last = $('div.control-group').last();
}
if(!last || last.length < 1 ){
last = $('div.control-group');
}
last.after(markup);
}
function annotate_profile() {
$('.annotated').remove();
var in_data = {'id':'', 'type': 'profile'};
var uri_data = get_urls(in_data);
id = in_data.id;
type = in_data.type;
if(!id) return;
$.ajax({
url: uri_data.uri_lines,
}).done(function(data) {
profileExtensionsData = data.aaData;
var ranges = profileExtensionsData.ranges;
create_ranges_annotations(id,'profile',profileExtensionsData.ranges);
process_extensions();
});
}
function process_extensions(){
var fieldExtensionsCurrent = get_extensions_current();
for( extension_unit in fieldExtensionsCurrent ){
extensionModelDeviceId = fieldExtensionsCurrent[extension_unit];
create_ranges_annotations(extensionModelDeviceId,'extension',profileExtensionsData.extensions[extensionModelDeviceId].ranges);
}
}
function get_extensions_current(){
//a little bit not optimal
var fieldExtensionsCurrent = {};
var profileModelDeviceId = profileExtensionsData.device.id;
for(extensionModelDeviceId in fieldExtensions){
if(fieldExtensions[extensionModelDeviceId]['availablePhones'][profileModelDeviceId]){
for(extension_unit in fieldExtensions[extensionModelDeviceId]['units']){
fieldExtensionsCurrent[extension_unit] = extensionModelDeviceId;
}
}
.annotated select.subselect
{
width: 180px;
}
return fieldExtensionsCurrent;
}
function add_extension_button(){
var fieldExtensionsNum = get_extensions_num();
var allowedExtensionsNum = profileExtensionsData.device.extensions_num;
var addExtensionMarkup = '';
var availableExtensionsNum = Object.keys(profileExtensionsData.extensions).length;
if(allowedExtensionsNum && availableExtensionsNum && (fieldExtensionsNum < allowedExtensionsNum)){
var exists = $('div#add_extension_button').length;
if(!exists){
addExtensionMarkup += '<div class="control-group" id="add_extension_button">'
+'<label class="control-label" for="extension">Device Extension</label>'
+'<div class="controls">'
+'<div class="control-group">'
+'<label class="control-label" for="extension.'+fieldExtensionsNum+'"></label>'
+'<div class="controls"><select name="extension_id" id="extension_id">';
for( extensionModelDeviceId in profileExtensionsData.extensions ){
addExtensionMarkup += '<option value='+extensionModelDeviceId+'>'+profileExtensionsData.extensions[extensionModelDeviceId].vendor+' '+ profileExtensionsData.extensions[extensionModelDeviceId].model;
}
addExtensionMarkup += '</select></div>'
+'</div>'
+'<div class="control-group">'
+'<div class="controls">'
+'<div class="btn btn-primary pull-right add_element btn" data-rep-elem-id="'+fieldExtensionsNum+'" id="extension.'+fieldExtensionsNum+'.add" onclick="var selectObj = document.getElementById(\'extension_id\');var extensionModelDeviceId=selectObj.options[selectObj.selectedIndex].value; create_ranges_annotations(extensionModelDeviceId,\'extension\',profileExtensionsData.extensions[extensionModelDeviceId].ranges);">Add Extension</div>'
+'</div>'
+'</div></div>'
+'</div>';
apply_markup(addExtensionMarkup);
}
.annotated select.modeselect
{
width: 60px;
}else{
$('div#add_extension_button').remove();
}
}
function get_linecmt(){
var linecmt = [];
$("select[id^='line.']").each(function (i, element) {
var found = element.id.match(/line\.(\d+)\.type/);
if(found && found[1]){
linecmt.push(found[1]);
}
</style>
<link rel="stylesheet" href="/css/ngcp-annotate.css">
<script src="/js/libs/ngcp-annotate.js"></script>
<link rel="stylesheet" type="text/css" href="/font/font-awesome/css/font-awesome.min.css"/>
<script>
var aaData;
function annotate_device() {
$('.annotated').remove();
var prof_id = $('div.controls #profile_id option:selected').first().attr('value');
if(!prof_id) return;
$.ajax({
url: "/device/profile/" + prof_id + "/annolines/ajax",
}).done(function(data) {
aaData = data.aaData;
console.log("got data", data);
var markup = '<div class="annotated">' +
'<img src="/device/profile/' + prof_id + '/frontimage" />';
var formcnt = -1;
for(var i = 0; i < aaData.length; ++i) {
var range = aaData[i];
for(var j = 0; j < range.annotations.length; ++j) {
formcnt++;
$('#line\\.' + formcnt).remove();
var a = range.annotations[j];
[%MACRO unassigned_form BLOCK%]
status = "unassigned";
action = '<i class="fa fa-plus-square fa-fw"></i> ' +
'<select class="subselect" name="line.' + formcnt + '.subscriber_id" id="line.' + formcnt + '.subscriber_id">' +
'<option value="0">[% c.loc("Subscriber") %]</option>' +
[% subs = [] -%]
[% FOR sub IN subs.merge(pbx_groups.all, subscribers.all) -%]
'<option value="[% sub.provisioning_voip_subscriber.id %]">[% sub.provisioning_voip_subscriber.display_name ? sub.provisioning_voip_subscriber.display_name : sub.username %][% sub.provisioning_voip_subscriber.pbx_extension.defined ? " (" _ sub.provisioning_voip_subscriber.pbx_extension _ ")" : "" %]</option>' +
[% END -%]
'</select>' +
'<select class="modeselect" name="line.' + formcnt + '.type" id="line.' + formcnt + '.type">' +
[% FOR opt IN ["private", "shared", "blf"] -%]
(range.can_[% opt %] == "1" ? '<option value="[% opt %]">[% opt %]</option>' : '') +
[% END -%]
'</select>' +
'<input type="hidden" name="line.' + formcnt + '.line" id="line.' + formcnt + '.line" value="' + a.range_id + '.' + a.line_index + '"/>' +
'';
[%END%]
[% IF create_flag == 1 -%]
var status, action;
[% unassigned_form() %]
[% ELSIF edit_flag == 1 -%]
var linekeys = {
[% FOR line IN pbx_device.autoprov_field_device_lines.all -%]
'[% line.linerange_id %].[% line.key_num %]': {
'mode': '[% line.line_type %]',
'sub': '[% line.provisioning_voip_subscriber.username %]@[% line.provisioning_voip_subscriber.domain.domain%]',
'subid': [% line.provisioning_voip_subscriber.id %],
'ext': '[% line.provisioning_voip_subscriber.pbx_extension%]'
} [% line == pbx_device.autoprov_field_device_lines.all.last ? '' : ',' %]
[% END -%]
};
var status, action;
var idx = aaData[i].id + '.' + a.line_index;
if(linekeys[idx]) {
status = "assigned";
//action = '<i class="fa ' + mode + ' fa-fw"></i> ' + linekeys[idx].sub + ' (' + linekeys[idx].ext + ')';
var action = '<i class="fa fa-user fa-fw"></i> ' +
'<select class="subselect" name="line.' + formcnt + '.subscriber_id" id="line.' + formcnt + '.subscriber_id">' +
'<option value="0">[% c.loc("None") %]</option>' +
[% subs = [] -%]
[% FOR sub IN subs.merge(pbx_groups.all, subscribers.all) -%]
'<option value="[% sub.provisioning_voip_subscriber.id %]"' + (linekeys[idx].subid == [% sub.provisioning_voip_subscriber.id %] ? ' selected="selected"' : '') + '>[% sub.provisioning_voip_subscriber.display_name ? sub.provisioning_voip_subscriber.display_name : sub.username %][% sub.provisioning_voip_subscriber.pbx_extension.defined ? " (" _ sub.provisioning_voip_subscriber.pbx_extension _ ")" : "" %]</option>' +
[% END -%]
'</select>' +
'<select class="modeselect" name="line.' + formcnt + '.type" id="line.' + formcnt + '.type">' +
[% FOR opt IN ["private", "shared", "blf"] -%]
(range.can_[% opt %] == "1" ?
'<option value="[% opt %]"' + (linekeys[idx].mode == "[% opt %]" ? ' selected="selected"' : '') + '>[% opt %]</option>' : '') +
[% END -%]
'</select>' +
'<input type="hidden" name="line.' + formcnt + '.line" id="line.' + formcnt + '.line" value="' + a.range_id + '.' + a.line_index + '"/>' +
'';
} else {
[% unassigned_form() %]
}
[% END -%]
markup += '<div class="caption ' + status + '" style="top:' + a.y + 'px; left:' + a.x + 'px;" data-pos="' + a.position + '">' + action + '</div>';
}
}
markup += '</div>';
$('div.control-group').last().after(markup);
$("div.annotated").each(function(){
$(this).find("div.caption").annotate(this);
});
});
});
var linecmt_max = Math.max.apply(Math, linecmt);
if(isNaN(linecmt_max) || linecmt_max == Number.POSITIVE_INFINITY || linecmt_max == Number.NEGATIVE_INFINITY){
//we shouldn't make -1 for the NEXT number, although in the code the first operation is ++ in the loop;
linecmt_max = -1;
}
return linecmt_max;
}
function get_extension_divcnt(id_in){
var divcnt = {};
$("div[id^='annotated_extension_']").each(function (i, element) {
var found = element.id.match(/annotated_extension_(\d+)_(\d+)/);
if(found && found[1] && found[2]){
if(!divcnt[found[1]]){
divcnt[found[1]] = [found[2]];
}else{
divcnt[found[1]].push(found[2]);
}
}
$('div.controls #profile_id').change(annotate_device);
annotate_device();
</script>
[% END -%]
});
for(id in divcnt){
divcnt[id] = Math.max.apply(Math, divcnt[id]);
}
var res = 0 + ( divcnt[id_in] ? divcnt[id_in] : 0 );
return res;
}
function get_extensions_num(){
return $("div[id^='annotated_extension_']").length;
}
$('div.controls #profile_id').change(annotate_profile);
annotate_profile();
</script>
<div id="extensions">
</div>
[% END -%]<!--/IF c.config.features.cloudpbx && device_flag-->
[% # vim: set tabstop=4 syntax=html expandtab: -%]

@ -8,10 +8,28 @@
[% back_created = 1 -%]
<script>
function typeDynamicFields(selectedValue){
$('.ngcp-devicetype').css("display","none");
$('.ngcp-devicetype-'+selectedValue).css("display","block");
if(selectedValue == 'phone'){
vendor2bootstrapMethod();
}
}
function bootstrapDynamicFields(selectedValue){
$('.ngcp-bootstrap-config').css("display","none");
$('.ngcp-bootstrap-config-'+selectedValue).css("display","block");
}
function type2formFields(){
var typeFields = document.getElementsByName('type');
var typeField;
if(typeFields && typeFields.length){
typeField = typeFields[0];//we will use default defined in Perl module to show linked fields
if(typeField){
var type = typeField.options[typeField.selectedIndex].value;
typeDynamicFields(type);
}
}
}
function vendor2bootstrapMethod(vendorField){
var bootstrapMethodField;
if(!vendorField){//initial load - no vendor field passed at all
@ -38,7 +56,7 @@ function vendor2bootstrapMethod(vendorField){
}
if(bootstrapMethod){
var length = bootstrapMethodField.options.length;
for(var i=0; i < length; i++){
for(var i=0; i < length; i++){
if(bootstrapMethodField.options[i].value == bootstrapMethod){
bootstrapMethodField.options[i].selected = true;
}
@ -50,7 +68,7 @@ function vendor2bootstrapMethod(vendorField){
bootstrapDynamicFields(bootstrapMethod);
}
$( document ).ready(function() {
vendor2bootstrapMethod();
type2formFields();
});
</script>

@ -4,9 +4,11 @@
var checked_fields = {};
$(document).ready(function() {
[%IF value%]
JSON.parse('[% value %]').map( function (val) {
checked_fields[val] = 1;
});
[%END%]
$('#[% table_id %] tr td input[type="checkbox"]').live( "click", function() {
var my_id = $(this).parents("tr").find("td:first").text();
@ -70,7 +72,7 @@ $(document).ready(function() {
} );
</script>
<div class="control-group [% IF errors.size %]error[% END %]">
<div class="control-group [% IF errors.size %]error[% END %] [%wrapper_class%]">
<label class="control-label" for="[% table_id %]">[% label %]</label>
<div class="controls">
<input type="hidden" name="[% field_name %]" value="[% value | html %]" id="[% hidden_id %]"/>

@ -0,0 +1,143 @@
#use Sipwise::Base;
use strict;
#use Moose;
use Sipwise::Base;
extends 'NGCP::Panel::Utils::Test::Collection';
use Net::Domain qw(hostfqdn);
use LWP::UserAgent;
use HTTP::Request::Common;
use JSON;
use Test::More;
use Data::Dumper;
use File::Basename;
#init test_machine
my $test_machine = NGCP::Panel::Utils::Test::Collection->new(
name => 'pbxdevicemodels',
embedded => [qw/pbxdevicefirmwares/]
);
@{$test_machine->content_type}{qw/POST PUT/} = (('multipart/form-data') x 2);
$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)};
$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH)};
#for item creation test purposes /post request data/
$test_machine->DATA_ITEM_STORE({
json => {
"model"=>"ATA111",
#should be some fake reseller - create reseller/customer/subscriber tests?
#"reseller_id"=>"1",
"vendor"=>"Cisco",
#3.7relative tests
"bootstrap_method"=>"http",
"bootstrap_uri"=>"",
"bootstrap_config_http_sync_method"=>"GET",
"bootstrap_config_http_sync_params"=>"[% server.uri %]/\$MA",
"bootstrap_config_http_sync_uri"=>"http=>//[% client.ip %]/admin/resync",
"bootstrap_config_redirect_panasonic_password"=>"",
"bootstrap_config_redirect_panasonic_user"=>"",
"bootstrap_config_redirect_polycom_password"=>"",
"bootstrap_config_redirect_polycom_profile"=>"",
"bootstrap_config_redirect_polycom_user"=>"",
"bootstrap_config_redirect_yealink_password"=>"",
"bootstrap_config_redirect_yealink_user"=>"",
"type"=>"phone",
#"connectable_models"=>[702,703,704],
"extensions_num"=>"2",
#/3.7relative tests
"linerange"=>[
{
"keys"=>[
{"y"=>"390","labelpos"=>"left","x"=>"510"},
{"y"=>"350","labelpos"=>"left","x"=>"510"}
],
"can_private"=>"1",
"can_shared"=>"0",
"can_blf"=>"0",
"name"=>"Phone Ports",
#test duplicate creation #"id"=>1311,
}
]
},
#can check big files
#'front_image' => [ dirname($0).'/resources/api_devicemodels_front_image.jpg' ],
'front_image' => [ dirname($0).'/resources/empty.txt' ],
});
##$test_machine->form_data_item( sub {$_[0]->{json}->{type} = "extension";} );
##$test_machine->check_create_correct( 1, sub{ $_[0]->{json}->{model} .= "Extension 2".$_[1]->{i}; } );
#$test_machine->check_get2put( sub {
# $_[0]->{connectable_models} = [670],
# $_[0] = {
# json => JSON::to_json($_[0]),
# 'front_image' => $test_machine->DATA_ITEM_STORE->{front_image}
# }; },
# $test_machine->get_uri_collection.'449'
#);
#
##test check_patch_prefer_wrong is broken
##$test_machine->name('billingprofiles');
##$test_machine->check_patch_prefer_wrong;
foreach my $type(qw/phone extension/){
#last;#skip classic tests
$test_machine->form_data_item( sub {$_[0]->{json}->{type} = $type;} );
# create 6 new billing models from DATA_ITEM
#$test_machine->check_create_correct( 6, sub{ $_[0]->{json}->{model} .= $type."TEST_".$_[1]->{i}; } );
#$test_machine->check_get2put( sub { $_[0] = { json => JSON::to_json($_[0]), 'front_image' => $test_machine->DATA_ITEM_STORE->{front_image} }; } );
$test_machine->check_bundle();
# try to create model without reseller_id
{
my ($res, $err) = $test_machine->request_post(sub{delete $_[0]->{json}->{reseller_id};});
is($res->code, 422, "create model without reseller_id");
is($err->{code}, "422", "check error code in body");
ok($err->{message} =~ /field='reseller_id'/, "check error message in body");
}
# try to create model with empty reseller_id
{
my ($res, $err) = $test_machine->request_post(sub{$_[0]->{json}->{reseller_id} = undef;});
is($res->code, 422, "create model with empty reseller_id");
is($err->{code}, "422", "check error code in body");
ok($err->{message} =~ /field='reseller_id'/, "check error message in body");
}
# try to create model with invalid reseller_id
{
my ($res, $err) = $test_machine->request_post(sub{$_[0]->{json}->{reseller_id} = 99999;});
is($res->code, 422, "create model with invalid reseller_id");
is($err->{code}, "422", "check error code in body");
ok($err->{message} =~ /Invalid reseller_id/, "check error message in body");
}
{
my (undef, $item_first_get) = $test_machine->check_item_get;
ok(exists $item_first_get->{reseller_id} && $item_first_get->{reseller_id}->is_int, "check existence of reseller_id");
foreach(qw/vendor model/){
ok(exists $item_first_get->{$_}, "check existence of $_");
}
# check if we have the proper links
# TODO: fees, reseller links
#ok(exists $new_contract->{_links}->{'ngcp:resellers'}, "check put presence of ngcp:resellers relation");
}
{
my $t = time;
my($res,$mod_model) = $test_machine->check_patch_correct( [ { op => 'replace', path => '/model', value => 'patched model '.$t } ] );
is($mod_model->{model}, "patched model $t", "check patched replace op");
}
{
my($res) = $test_machine->request_patch( [ { op => 'replace', path => '/reseller_id', value => undef } ] );
is($res->code, 422, "check patched undef reseller");
}
{
my($res) = $test_machine->request_patch( [ { op => 'replace', path => '/reseller_id', value => 99999 } ] );
is($res->code, 422, "check patched invalid reseller");
}
}
`echo 'delete from autoprov_devices where model like "%TEST\\_%" or model like "patched model%";'|mysql provisioning`;
done_testing;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,59 @@
#use Sipwise::Base;
use strict;
#use Moose;
use Sipwise::Base;
extends 'NGCP::Panel::Utils::Test::Collection';
use Net::Domain qw(hostfqdn);
use LWP::UserAgent;
use HTTP::Request::Common;
use JSON;
use Test::More;
use Data::Dumper;
use File::Basename;
use bignum qw/hex/;
#init test_machine
my $test_machine = NGCP::Panel::Utils::Test::Collection->new(
name => 'pbxdevices',
embedded => [qw/pbxdeviceprofiles customers/]
);
$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)};
$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)};
#for item creation test purposes /post request data/
$test_machine->DATA_ITEM_STORE({
#'profile_id' => '151',
#somehow should obtain/create test customer with the test subscriber - discuss with alex
#'customer_id' => '968',
'identifier' => 'aaaabbbbcccc',
'station_name' => 'abc',
'lines'=>[{
'linerange' => 'Phone Ports',
'type' => 'private',
'key_num' => '0',
#somehow should obtain/create test customer with the test subscriber - discuss with alex
#'subscriber_id' => '1198',
'extension_unit' => '1',
#'extension_num' => '1',#to handle some the same extensions devices
},{
'linerange' => 'Phone Ports',
'type' => 'private',
'key_num' => '1',
#somehow should obtain/create test customer with the test subscriber - discuss with alex
#'subscriber_id' => '1198',
'extension_unit' => '2',
}],
});
$test_machine->form_data_item( );
# create 3 new billing models from DATA_ITEM
#$test_machine->check_create_correct( 3, sub{ $_[0]->{identifier} = sprintf('%x', (hex('0x'.$_[0]->{identifier}) + $_[1]->{i}) ); } );
#$test_machine->check_get2put( );
$test_machine->check_bundle();
$test_machine->check_delete_use_created();
done_testing;
# vim: set tabstop=4 expandtab:

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Loading…
Cancel
Save