TT#41628 Add web ui for the TimeSets

Add test script for API
Fix DateTime create/update issue in TimeSets API (get2put case)
    (add and edit functionlity used raw/inflated data respecively)
Add datetimepicker field

Change-Id: If724b7350658c306dbbecbc04309d1d1c0b4a3e2
changes/41/23941/8
Irina Peshinskaya 8 years ago
parent b65374e8ac
commit e462e95957

@ -5,7 +5,11 @@ use Sipwise::Base;
use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::TimeSets/;
use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::TimeSet;
__PACKAGE__->set_config({
allowed_roles => [qw/admin reseller/],
});
sub allowed_methods{
return [qw/GET POST OPTIONS HEAD/];
@ -31,10 +35,6 @@ sub query_params {
];
}
__PACKAGE__->set_config({
allowed_roles => [qw/admin reseller/],
});
sub create_item {
my ($self, $c, $resource, $form, $process_extras) = @_;
@ -43,15 +43,7 @@ sub create_item {
try {
# # no checks, they are in check_resource
$tset = $schema->resultset('voip_time_sets')->create({
name => $resource->{name},
reseller_id => $resource->{reseller_id},
});
for my $t ( @{$resource->{times}} ) {
$tset->create_related("time_periods", {
%{ $t },
});
}
$tset = NGCP::Panel::Utils::TimeSet::create_timesets( c => $c, resource => $resource );
} catch($e) {
$c->log->error("failed to create timeset: $e");
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create timeset.");

@ -6,6 +6,14 @@ use parent qw/NGCP::Panel::Role::EntitiesItem NGCP::Panel::Role::API::TimeSets/;
use HTTP::Status qw(:constants);
__PACKAGE__->set_config({
allowed_roles => {
Default => [qw/admin reseller/],
Journal => [qw/admin reseller/],
},
PATCH => { ops => [qw/add replace remove copy/] },
});
sub allowed_methods{
return [qw/GET OPTIONS HEAD PATCH PUT DELETE/];
}
@ -15,14 +23,6 @@ sub journal_query_params {
return $self->get_journal_query_params($query_params);
}
__PACKAGE__->set_config({
allowed_roles => {
Default => [qw/admin reseller/],
Journal => [qw/admin reseller/],
},
PATCH => { ops => [qw/add replace remove copy/] },
});
sub get_journal_methods{
return [qw/handle_item_base_journal handle_journals_get handle_journalsitem_get handle_journals_options handle_journalsitem_options handle_journals_head handle_journalsitem_head/];
}

@ -18,6 +18,7 @@ use NGCP::Panel::Utils::BillingMappings qw();
use NGCP::Panel::Utils::Billing qw();
use NGCP::Panel::Utils::Admin;
use NGCP::Panel::Utils::Phonebook;
use NGCP::Panel::Utils::TimeSet;
sub auto :Private {
my ($self, $c) = @_;
@ -158,7 +159,18 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) {
return;
}
$c->detach('/denied_page')
if($c->user->roles eq "reseller" && $c->user->reseller_id != $reseller_id);
if($c->user->roles eq "reseller" && $c->user->reseller_id != $reseller_id);
my $reseller = $c->stash->{resellers}->search_rs({ id => $reseller_id });
unless($reseller->first) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Reseller not found',
desc => $c->loc('Reseller not found'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/reseller'));
}
$c->stash(reseller => $reseller);
$c->stash->{contact_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc('#') },
@ -205,7 +217,7 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) {
{ name => "name", "search" => 1, "title" => $c->loc("Name") },
#{ name => "reseller.name", "search" => 1, "title" => $c->loc("Reseller") },
#{ name => "v_count_used", "search" => 0, "title" => $c->loc("Used") },
NGCP::Panel::Utils::Billing::get_datatable_cols($c),
NGCP::Panel::Utils::Billing::get_datatable_cols($c),
]);
$c->stash->{network_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
@ -228,17 +240,13 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) {
{ name => "number", search => 1, title => $c->loc("Number") },
]);
$c->stash(reseller => $c->stash->{resellers}->search_rs({ id => $reseller_id }));
unless($c->stash->{reseller}->first) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Reseller not found',
desc => $c->loc('Reseller not found'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/reseller'));
}
$c->stash->{branding} = $c->stash->{reseller}->first->branding;
$c->stash->{phonebook} = $c->stash->{reseller}->first->phonebook;
$c->stash->{timeset_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "name", search => 1, title => $c->loc("Name") },
]);
$c->stash->{timesets_rs} = $reseller->first->time_sets;
$c->stash->{branding} = $reseller->first->branding;
$c->stash->{phonebook} = $reseller->first->phonebook;
}
sub reseller_contacts :Chained('base') :PathPart('contacts/ajax') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) {
@ -436,10 +444,10 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
my ($self, $c) = @_;
$c->detach('/denied_page') unless $c->request->method eq 'POST';
$c->detach('/denied_page')
if($c->user->read_only);
if($c->user->read_only);
my $default_pass = 'defaultresellerpassword';
my $saltedpass = NGCP::Panel::Utils::Admin::generate_salted_hash($default_pass);
my $default_pass = 'defaultresellerpassword';
my $saltedpass = NGCP::Panel::Utils::Admin::generate_salted_hash($default_pass);
my $billing = $c->model('DB');
my $now = NGCP::Panel::Utils::DateTime::current_local;
@ -461,7 +469,7 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
status => 'active',
},
admins => {
saltedpass => $saltedpass,
saltedpass => $saltedpass,
is_active => 1,
show_passwords => 1,
call_data => 1,
@ -853,6 +861,166 @@ sub phonebook_download_csv :Chained('base') :PathPart('phonebook_download_csv')
return;
}
sub timeset_ajax :Chained('base') :PathPart('reseller/ajax') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
NGCP::Panel::Utils::Datatables::process($c,
@{$c->stash}{qw(timesets_rs timeset_dt_columns)});
$c->detach( $c->view("JSON") );
}
sub timeset_create :Chained('base') :PathPart('timeset/create') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
my $reseller = $c->stash->{reseller}->first;
my $posted = ($c->request->method eq 'POST');
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::Reseller", $c);
my $params = {};
$params = merge($params, $c->session->{created_objects});
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
my $resource = $form->values;
$resource->{reseller_id} = $reseller->id;
$c->model('DB')->schema->txn_do( sub {
NGCP::Panel::Utils::TimeSet::create_timesets(
c => $c,
resource => $resource,
);
});
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Timeset entry successfully created'),
);
} catch ($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to create timeset entry.'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for_action("/reseller/details", [$reseller->id]));
}
$c->stash(
close_target => $c->uri_for_action("/reseller/details", [$reseller->id]),
create_flag => 1,
form => $form
);
}
sub timeset_base :Chained('base') :PathPart('timeset') :CaptureArgs(1) {
my ($self, $c, $timeset_id) = @_;
unless($timeset_id && is_int($timeset_id)) {
$timeset_id //= '';
NGCP::Panel::Utils::Message::error(
c => $c,
data => { id => $timeset_id },
desc => $c->loc('Invalid timeset id detected'),
);
$c->response->redirect($c->uri_for());
$c->detach;
return;
}
my $rs = $c->stash->{reseller}->first->time_sets->find($timeset_id);
unless(defined($rs)) {
NGCP::Panel::Utils::Message::error(
c => $c,
desc => $c->loc('Timeset entry does not exist'),
);
$c->response->redirect($c->uri_for());
$c->detach;
return;
}
$c->stash(
timeset => {$rs->get_inflated_columns},
timeset_rs => $rs
);
}
sub timeset_edit :Chained('timeset_base') :PathPart('edit') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
my $reseller = $c->stash->{reseller}->first;
my $posted = ($c->request->method eq 'POST');
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::Reseller", $c);
my $params = NGCP::Panel::Utils::TimeSet::get_timeset(c => $c, timeset => $c->stash->{timeset_rs});
$params = merge($params, $c->session->{created_objects});
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
if($posted && $form->validated) {
try {
$c->model('DB')->schema->txn_do( sub {
my $resource = $form->values;
$resource->{reseller_id} = $reseller->id;
NGCP::Panel::Utils::TimeSet::update_timesets(
c => $c,
timeset => $c->stash->{'timeset_rs'},
resource => $resource,
);
});
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Timeset entry successfully updated'),
);
} catch ($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to update timeset entry'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for_action("/reseller/details", [$reseller->id]));
}
$c->stash(
close_target => $c->uri_for_action("/reseller/details", [$reseller->id]),
edit_flag => 1,
form => $form
);
}
sub timeset_delete :Chained('timeset_base') :PathPart('delete') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
my $reseller = $c->stash->{reseller}->first;
my $timeset = $c->stash->{timeset};
try {
$c->stash->{timeset_rs}->delete;
NGCP::Panel::Utils::Message::info(
c => $c,
data => $c->stash->{timeset},
desc => $c->loc('Timeset entry successfully deleted'),
);
} catch ($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
data => $c->stash->{timeset},
desc => $c->loc('Failed to delete timeset entry'),
);
};
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for_action("/reseller/details", [$reseller->id]));
}
1;

@ -0,0 +1,217 @@
package NGCP::Panel::Controller::TimeSet;
use NGCP::Panel::Utils::Generic qw(:all);
use Sipwise::Base;
use parent 'Catalyst::Controller';
use NGCP::Panel::Form;
use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::TimeSet;
sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) {
my ($self, $c) = @_;
$c->log->debug(__PACKAGE__ . '::auto');
NGCP::Panel::Utils::Navigation::check_redirect_chain(c => $c);
return 1;
}
sub list :Chained('/') :PathPart('timeset') :CaptureArgs(0) {
my ( $self, $c ) = @_;
$c->stash->{timesets_rs} = $c->model('DB')->resultset('voip_time_sets');
unless($c->user->roles eq "admin") {
$c->stash->{timesets_rs} = $c->stash->{timesets_rs}->search({
reseller_id => $c->user->reseller_id
});
}
$c->stash->{timeset_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => 'id', search => 1, title => $c->loc('#') },
$c->user->roles eq "admin"
? { name => 'reseller.name', search => 1, title => $c->loc('Reseller') }
: (),
{ name => 'name', search => 1, title => $c->loc('Name') },
]);
$c->stash(template => 'timeset/list.tt');
}
sub root :Chained('list') :PathPart('') :Args(0) {
my ($self, $c) = @_;
}
sub ajax :Chained('list') :PathPart('ajax') :Args(0) {
my ($self, $c) = @_;
my $rs = $c->stash->{timesets_rs};
NGCP::Panel::Utils::Datatables::process($c, $rs, $c->stash->{timeset_dt_columns});
$c->detach( $c->view("JSON") );
}
sub base :Chained('list') :PathPart('') :CaptureArgs(1) {
my ($self, $c, $timeset_id) = @_;
unless($timeset_id && is_int($timeset_id)) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Invalid timeset enry id detected',
desc => $c->loc('Invalid timeset entry id detected'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/timeset'));
}
my $rs = $c->stash->{timesets_rs}->find($timeset_id);
unless(defined($rs)) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Timeset entry does not exist',
desc => $c->loc('Timeset entry does not exist'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/timeset'));
}
$c->stash(
timeset => {$rs->get_inflated_columns},
timeset_rs => $rs
);
}
sub create :Chained('list') :PathPart('create') :Args(0) {
my ($self, $c) = @_;
my $posted = ($c->request->method eq 'POST');
my $params = {};
$params = merge($params, $c->session->{created_objects});
my $form;
if($c->user->roles eq "admin") {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::Admin", $c);
} else {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::Reseller", $c);
}
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
fields => {
'reseller.create' => $c->uri_for('/reseller/create'),
},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
my $resource = $form->values;
if($c->user->roles eq "admin") {
$resource->{reseller_id} = $form->values->{reseller}{id};
delete $resource->{reseller};
} else {
$resource->{reseller_id} = $c->user->reseller_id;
}
$c->model('DB')->schema->txn_do( sub {
NGCP::Panel::Utils::TimeSet::create_timesets(
c => $c,
resource => $form->values,
);
});
delete $c->session->{created_objects}->{reseller};
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Timeset entry successfully created'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to create timeset entry'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/timeset'));
}
$c->stash(form => $form);
$c->stash(create_flag => 1);
}
sub edit :Chained('base') :PathPart('edit') {
my ($self, $c) = @_;
my $posted = ($c->request->method eq 'POST');
my $params = NGCP::Panel::Utils::TimeSet::get_timeset(c => $c, timeset => $c->stash->{timeset_rs});
$params->{reseller}{id} = delete $params->{reseller_id};
$params = merge($params, $c->session->{created_objects});
my $form;
if($c->user->roles eq "admin") {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::Admin", $c);
} else {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::Timeset::Reseller", $c);
}
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
fields => {
'reseller.create' => $c->uri_for('/reseller/create'),
},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
$c->model('DB')->schema->txn_do( sub {
my $resource = $form->values;
if($c->user->roles eq "admin") {
$resource->{reseller_id} = $form->values->{reseller}{id};
delete $resource->{reseller};
} else {
$resource->{reseller_id} = $c->user->reseller_id;
}
NGCP::Panel::Utils::TimeSet::update_timesets(
c => $c,
timeset => $c->stash->{timeset_rs},
resource => $resource,
);
});
delete $c->session->{created_objects}->{reseller};
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Timeset entry successfully updated'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to update timeset entry'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/timeset'));
}
$c->stash(form => $form);
$c->stash(edit_flag => 1);
}
sub delete_timeset :Chained('base') :PathPart('delete') {
my ($self, $c) = @_;
try {
$c->stash->{timeset_rs}->delete;
NGCP::Panel::Utils::Message::info(
c => $c,
data => $c->stash->{timeset},
desc => $c->loc('Timeset entry successfully deleted'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to delete timeset entry'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/timeset'));
}
1;

@ -0,0 +1,64 @@
package NGCP::Panel::Field::DateTimePicker;
use HTML::FormHandler::Moose;
use Template;
extends 'HTML::FormHandler::Field';
has '+widget' => (default => ''); # leave this empty, as there is no widget ...
has 'template' => ( isa => 'Str',
is => 'rw',
default => 'helpers/datetimepicker.tt' );
has 'language_file' => (isa => 'Str', is => 'rw', default => 'dataTables.default.js' );
has 'date_format_js' => (isa => 'Str', is => 'rw', default => 'yy-mm-dd' );
has 'time_format_js' => (isa => 'Str', is => 'rw', default => 'HH:mm:ss' );
sub render_element {
my ($self) = @_;
my $output = '';
(my $fieldname = $self->html_name) =~ s!\.!!g;
my $vars = {
label => $self->label,
field_name => $self->html_name,
field_id => $fieldname . "_datetimepicker",
value => $self->value,
date_format_js => $self->date_format_js,
time_format_js => $self->time_format_js,
errors => $self->errors,
language_file => $self->language_file,
};
my $t = new Template({
ABSOLUTE => 1,
INCLUDE_PATH => [
'/usr/share/ngcp-panel/templates',
'share/templates',
],
});
$t->process($self->template, $vars, \$output) or
die "Failed to process DateTimePicker field template: ".$t->error();
return $output;
}
sub render {
my ( $self, $result ) = @_;
$result ||= $self->result;
die "No result for form field '" . $self->full_name . "'. Field may be inactive." unless $result;
return $self->render_element( $result );
}
sub validate {
my ( $self ) = @_;
if($self->required &&
( !defined $self->value || !length($self->value) || $self->value !~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/ ) ) {
return $self->add_error("Invalid datetime, must be in format YYYY-mm-DD HH:MM:SS");
}
return 1;
}
no Moose;
1;
# vim: set tabstop=4 expandtab:

@ -1,4 +1,4 @@
package NGCP::Panel::Form::IcalTimeSet;
package NGCP::Panel::Form::TimeSet::API;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
@ -33,75 +33,154 @@ has_field 'times.id' => (
has_field 'times.start' => (
type => '+NGCP::Panel::Field::DateTime',
label => 'Start',
required => 1,
);
has_field 'times.end' => (
type => '+NGCP::Panel::Field::DateTime',
label => 'End',
);
has_field 'times.freq' => (
type => 'Select',
label => 'Frequency',
options => [
map { +{value => $_, label => $_}; } (qw/secondly minutely hourly daily weekly monthly yearly/)
],
);
has_field 'times.until' => (
label => 'Until',
type => '+NGCP::Panel::Field::DateTime',
element_attr => {
rel => ['tooltip'],
title => ['Can\'t be defined together with "Count".']
},
);
has_field 'times.count' => (
type => 'PosInteger',
label => 'Count',
element_attr => {
rel => ['tooltip'],
title => ['Valid value is a positive integer. Can\'t be defined together with "Until".']
},
);
has_field 'times.interval' => (
type => 'PosInteger',
label => 'Interval',
element_attr => {
rel => ['tooltip'],
title => ['Valid value is a positive integer.']
},
);
has_field 'times.bysecond' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By second',
min_value => 0,
max_value => 60,
max_value => 59,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 0 to 59, i.e. 1,3,59.']
},
);
has_field 'times.byminute' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By minute',
min_value => 0,
max_value => 59,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 0 to 59, i.e. 1,3,59.']
},
);
has_field 'times.byhour' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By hour',
min_value => 0,
max_value => 60,
max_value => 23,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 0 to 23, i.e. 1,3,23.']
},
);
has_field 'times.byday' => (
type => 'Text', # (\+|-)?\d*(MO|DI|MI|DO|FR|SA|SU)
label => 'By day',
element_attr => {
rel => ['tooltip'],
title => ['Value format is ~[+|-~]~[NUMBER~](MO|DI|MI|DO|FR|SA|SU). Example: 5FR (means fifth friday).']
},
# example: 5FR (means fifth friday)
);
has_field 'times.bymonthday' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By month day',
min_value => 1,
max_value => 31,
plusminus => 1,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 31, i.e. 1,3,31.']
},
);
has_field 'times.byyearday' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By year day',
min_value => 1,
max_value => 366,
plusminus => 1,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 366, i.e. 1,3,366.']
},
);
has_field 'times.byweekno' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By week number',
min_value => 1,
max_value => 53,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 53, i.e. 1,3,53.']
},
);
has_field 'times.bymonth' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By month',
min_value => 1,
max_value => 12,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 12, i.e. 1,3,12.']
},
);
has_field 'times.bysetpos' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By set position',
min_value => 1,
max_value => 366,
plusminus => 1,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 366, i.e. 1,3,366.']
},
);
has_field 'times.comment' => (
type => 'Text',
label => 'Comment',
);

@ -0,0 +1,23 @@
package NGCP::Panel::Form::TimeSet::Admin;
use HTML::FormHandler::Moose;
extends 'NGCP::Panel::Form::TimeSet::Reseller';
has_field 'reseller' => (
type => '+NGCP::Panel::Field::Reseller',
validate_when_empty => 1,
element_attr => {
rel => ['tooltip'],
title => ['The reseller id to assign this timeset entry to.']
},
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/id reseller name times times_add/],
);
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,259 @@
package NGCP::Panel::Form::TimeSet::Reseller;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
with 'NGCP::Panel::Render::RepeatableJs';
has '+widget_wrapper' => ( default => 'Bootstrap' );
has_field 'submitid' => ( type => 'Hidden' );
sub build_render_list {[qw/submitid fields actions/]}
sub build_form_element_class { [qw/form-horizontal/] }
has_field 'id' => (
type => 'Hidden',
);
has_field 'name' => (
type => 'Text',
label => 'Name',
required => 1,
);
has_field 'times' => (
type => 'Repeatable',
setup_for_js => 1,
do_wrapper => 1,
do_label => 1,
wrapper_class => [qw/hfh-rep-block/],
element_attr => {
rel => ['tooltip'],
title => ['An array of time definitions with a number of optional and mandatory keys.']
},
);
has_field 'times.id' => (
type => 'Hidden',
);
has_field 'times.start' => (
type => '+NGCP::Panel::Field::DateTimePicker',
label => 'Start',
required => 1,
);
has_field 'times.end' => (
type => '+NGCP::Panel::Field::DateTimePicker',
label => 'End',
);
has_field 'times.freq' => (
type => 'Select',
label => 'Frequency',
options => [
map { +{value => $_, label => $_}; } (qw/secondly minutely hourly daily weekly monthly yearly/)
],
);
has_field 'times.until' => (
label => 'Until',
type => '+NGCP::Panel::Field::DateTimePicker',
element_attr => {
rel => ['tooltip'],
title => ['Can\'t be defined together with "Count".']
},
);
has_field 'times.count' => (
type => 'PosInteger',
label => 'Count',
element_attr => {
rel => ['tooltip'],
title => ['Valid value is a positive integer. Can\'t be defined together with "Until".']
},
);
has_field 'times.interval' => (
type => 'PosInteger',
label => 'Interval',
element_attr => {
rel => ['tooltip'],
title => ['Valid value is a positive integer.']
},
);
has_field 'times.bysecond' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By second',
min_value => 0,
max_value => 59,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 0 to 59, i.e. 1,3,59.']
},
);
has_field 'times.byminute' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By minute',
min_value => 0,
max_value => 59,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 0 to 59, i.e. 1,3,59.']
},
);
has_field 'times.byhour' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By hour',
min_value => 0,
max_value => 23,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 0 to 23, i.e. 1,3,23.']
},
);
has_field 'times.byday' => (
type => 'Text', # (\+|-)?\d*(MO|DI|MI|DO|FR|SA|SU)
label => 'By day',
element_attr => {
rel => ['tooltip'],
title => ['Value format is ~[+|-~]~[NUMBER~](MO|DI|MI|DO|FR|SA|SU). Example: 5FR (means fifth friday).']
},
# example: 5FR (means fifth friday)
);
has_field 'times.bymonthday' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By month day',
min_value => 1,
max_value => 31,
plusminus => 1,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 31, i.e. 1,3,31.']
},
);
has_field 'times.byyearday' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By year day',
min_value => 1,
max_value => 366,
plusminus => 1,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 366, i.e. 1,3,366.']
},
);
has_field 'times.byweekno' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By week number',
min_value => 1,
max_value => 53,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 53, i.e. 1,3,53.']
},
);
has_field 'times.bymonth' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By month',
min_value => 1,
max_value => 12,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 12, i.e. 1,3,12.']
},
);
has_field 'times.bysetpos' => (
type => '+NGCP::Panel::Field::IntegerList',
label => 'By set position',
min_value => 1,
max_value => 366,
plusminus => 1,
element_attr => {
rel => ['tooltip'],
title => ['Value is set of numbers from 1 to 366, i.e. 1,3,366.']
},
);
has_field 'times.comment' => (
type => 'Text',
label => 'Comment',
);
has_field 'times.rm' => (
type => 'RmElement',
value => 'Remove Set',
order => 100,
element_class => [qw/btn btn-primary pull-right/],
);
has_field 'times_add' => (
type => 'AddElement',
repeatable => 'times',
value => 'Add another Set',
element_class => [qw/btn btn-primary pull-right/],
);
has_field 'save' => (
type => 'Submit',
value => 'Save',
element_class => [qw/btn btn-primary/],
label => '',
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/id name times times_add/],
);
has_block 'actions' => (
tag => 'div',
class => [qw/modal-footer/],
render_list => [qw/save/],
);
sub validate {
my ($self, $field) = @_;
my $c = $self->ctx;
return unless $c;
my $schema = $c->model('DB');
my $name = $self->field('name')->value;
my $reseller_id;
#Todo: to some utils?
if ($c->user->roles eq 'admin') {
if ($self->field('reseller')) {
$reseller_id = $self->field('reseller')->value;
} elsif($c->stash->{reseller}) { #strange, reseller interface keeps rs as reseller, not reseller_rs
$reseller_id = $c->stash->{reseller}->first->id;
}
} else {
$reseller_id = $c->user->reseller_id
}
unless ($reseller_id) {
#we shouldn't get here
$self->field('name')->add_error($c->loc('Unknow reseller'));
}
#/todo
my $existing_item = $schema->resultset('voip_time_sets')->find({
name => $name,
});
my $current_item = $c->stash->{timeset_rs};
if ($existing_item && (!$current_item || $existing_item->id != $current_item->id)) {
$self->field('name')->add_error($c->loc('This name already exists'));
}
}
1;
# vim: set tabstop=4 expandtab:

@ -8,7 +8,7 @@ use parent 'NGCP::Panel::Role::API';
use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Utils::TimeSet;
use NGCP::Panel::Form;
sub resource_name {
@ -17,15 +17,14 @@ sub resource_name {
sub get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::get("NGCP::Panel::Form::IcalTimeSet", $c);
return NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::API", $c);
}
sub hal_links{
my($self, $c, $item, $resource, $form) = @_;
my $adm = $c->user->roles eq "admin" || $c->user->roles eq "reseller";
return [
Data::HAL::Link->new(relation => "ngcp:resellers", href => sprintf("/api/resellers/%d", $resource->{reseller_id})),
$adm ? $self->get_journal_relation_link($item->id) : (),
Data::HAL::Link->new(relation => "ngcp:resellers", href => sprintf("/api/resellers/%d", $resource->{reseller_id})),
];
}
@ -48,20 +47,14 @@ sub _item_rs {
sub resource_from_item {
my ($self, $c, $item, $form) = @_;
my $resource = NGCP::Panel::Utils::TimeSet::get_timeset(
c => $c, timeset => $item, date_mysql_format => 1);
return $resource;
}
my $resource = { $item->get_inflated_columns };
my @periods;
for my $period ($item->time_periods->all) {
my $period_infl = { $period->get_inflated_columns, };
delete @{ $period_infl }{'time_set_id', 'id'};
for my $k (keys %{ $period_infl }) {
delete $period_infl->{$k} unless defined $period_infl->{$k};
}
push @periods, $period_infl;
}
$resource->{times} = \@periods;
sub process_form_resource{
my($self,$c, $item, $old_resource, $resource, $form, $process_extras) = @_;
$resource->{times} = $form->values->{times}; # not taking times from get_valid_data, but from form values, to benefit from formhandler inflation
return $resource;
}
@ -115,16 +108,11 @@ sub update_item_model {
my($self, $c, $item, $old_resource, $resource, $form) = @_;
try {
$item->update({
name => $resource->{name},
reseller_id => $resource->{reseller_id},
})->discard_changes;
$item->time_periods->delete;
for my $t ( @{ $form->values->{times} } ) { # not taking @{$resource->{times}}, to benefit from formhandler inflation
$item->create_related("time_periods", {
%{ $t },
});
}
NGCP::Panel::Utils::TimeSet::update_timesets(
c => $c,
timeset => $item,
resource => $resource,
);
$item->discard_changes;
} catch($e) {
$c->log->error("failed to update timeset: $e");

@ -252,6 +252,13 @@ sub from_string {
return $ts;
}
sub from_mysql_to_js{
my $s = shift;
$s =~ s/^(\d{4}\-\d{2}\-\d{2})T(\d.+)$/$1 $2/;
return $s;
}
sub from_rfc1123_string {
my $s = shift;
@ -266,22 +273,16 @@ sub from_rfc1123_string {
# it returns a DateTime object in floating (meaning local) timezone or the specified timezone
sub from_forminput_string {
my($string, $tz) = @_;
my $parser1 = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%d %H:%M:%S', $tz ? (time_zone => $tz) : (),
);
my $parser2 = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%d %H:%M', $tz ? (time_zone => $tz) : (),
);
my $parser3 = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%d', $tz ? (time_zone => $tz) : (),
);
$string =~ s/^\s*(.*)\s*$/$1/; # remove whitespace around
my $dt = $parser1->parse_datetime($string);
unless ($dt) {
$dt = $parser2->parse_datetime($string);
}
unless ($dt) {
$dt = $parser3->parse_datetime($string);
$string =~ s/T/ /; # replace T from API input
my ($dt);
foreach my $pattern ('%Y-%m-%d %H:%M:%S','%Y-%m-%d %H:%M','%Y-%m-%d') {
my $parser = DateTime::Format::Strptime->new(
pattern => $pattern,
$tz ? (time_zone => $tz) : (),
);
$dt = $parser->parse_datetime($string);
last if $dt;
}
return $dt;
}

@ -3,6 +3,7 @@ use strict;
use warnings;
use Sipwise::Base;
use NGCP::Panel::Utils::DateTime;
sub create_email_templates{
my %params = @_;
@ -152,9 +153,11 @@ sub _handle_reseller_status_change {
$reseller->emergency_containers->search_related_rs('emergency_mappings')->delete_all;
$reseller->emergency_containers->delete_all;
$reseller->voip_intercepts->delete_all;
$reseller->time_sets->delete_all;
}
}
1;
=head1 NAME

@ -0,0 +1,109 @@
package NGCP::Panel::Utils::TimeSet;
use strict;
use warnings;
use Sipwise::Base;
use NGCP::Panel::Utils::DateTime;
sub delete_timesets {
my %params = @_;
my($c, $timeset) = @params{qw/c timeset/};
$timeset->delete();
}
sub update_timesets {
my %params = @_;
my($c, $timeset, $resource) = @params{qw/c timeset resource/};
$timeset->update({
name => $resource->{name},
reseller_id => $resource->{reseller_id},
})->discard_changes;
$timeset->time_periods->delete;
for my $t ( @{$resource->{times} } ) {
$timeset->create_related("time_periods", {
%{ $t },
});
}
}
sub create_timesets {
my %params = @_;
my($c, $resource) = @params{qw/c resource/};
my $schema = $c->model('DB');
my $timeset = $schema->resultset('voip_time_sets')->create({
name => $resource->{name},
reseller_id => $resource->{reseller_id},
});
for my $t ( @{$resource->{times}} ) {
$timeset->create_related("time_periods", {
%{ $t },
});
}
return $timeset;
}
sub get_timeset {
my %params = @_;
my($c, $timeset, $date_mysql_format) = @params{qw/c timeset date_mysql_format/};
my $resource = { $timeset->get_inflated_columns };
my @periods;
for my $period ($timeset->time_periods->all) {
my $period_infl = { $period->get_inflated_columns, };
delete @{ $period_infl }{'time_set_id', 'id'};
if (!$date_mysql_format) {
foreach my $date_key (qw/start end until/) {
if (defined $period_infl->{$date_key}) {
$period_infl->{$date_key} = NGCP::Panel::Utils::DateTime::from_mysql_to_js($period_infl->{$date_key});
}
}
}
for my $k (keys %{ $period_infl }) {
delete $period_infl->{$k} unless defined $period_infl->{$k};
}
push @periods, $period_infl;
}
$resource->{times} = \@periods;
return $resource;
}
1;
=head1 NAME
NGCP::Panel::Utils::TimeSet
=head1 DESCRIPTION
A temporary helper to manipulate resellers data
=head1 METHODS
=head2 update_timesets
Update timesets database data
=head2 create_timesets
Create timesets database data
=head2 get_timesets
Get timesets data from database to show to the user
=head1 AUTHOR
Irina Peshinskaya
=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:

@ -0,0 +1,21 @@
<div class="control-group [% IF errors.size %]error[% END %]">
<script src="/js/libs/jquery-ui-timepicker-addon.js"></script>
[% date_format_js = date_format_js || 'yy-mm-dd' %]
[% time_format_js = time_format_js || 'HH:mm:ss' %]
<label class="control-label" for="[% field_id %]">[% label %]</label>
<div class="controls">
<input type="text" name="[% field_name %]" id="[% field_id %]" value="[%value%]" class="ngcp-datepicker" rel="tooltip" data-original-title="[% date_format_js %] [% time_format_js %]" onclick="
$(this).datetimepicker({
'dateFormat': '[% date_format_js %]',
'showSecond': true,
'timeFormat': '[% time_format_js %]',
});
$(this).datetimepicker('show');"/>
[% IF errors.size -%]
<span class="help-inline">
[% errors.join('<br/>') %]
</span>
[% END -%]
</div>
</div>
[% # vim: set tabstop=4 syntax=html expandtab: -%]

@ -439,6 +439,43 @@
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#reseller_details" href="#collapse_timeset">[% c.loc('Time sets') %]</a>
</div>
<div class="accordion-body collapse" id="collapse_timeset">
<div class="accordion-inner">
[%
helper.name = c.loc('Time sets');
helper.identifier = "TimeSets";
helper.dt_columns = timeset_dt_columns;
helper.messages = messages;
helper.close_target = close_target;
helper.create_flag = create_flag;
helper.edit_flag = edit_flag;
helper.form_object = form;
helper.ajax_uri = c.uri_for_action('/reseller/timeset_ajax', [c.req.captures.0] );
helper.tmpuri = c.uri_for(reseller.first.id, 'timeset');
UNLESS c.user.read_only;
helper.dt_buttons = [
{ name = c.loc('Edit'), uri = helper.tmpuri _ "/'+full.id+'/edit", class = 'btn-small btn-tertiary', icon = 'icon-edit' },
{ name = c.loc('Delete'), uri = helper.tmpuri _ "/'+full.id+'/delete", class = 'btn-small btn-secondary', icon = 'icon-remove' },
];
helper.top_buttons = [
{ name = c.loc('Create Time Set'), uri = c.uri_for_action('/reseller/timeset_create', [c.req.captures.0]), class = 'btn-small btn-primary', icon = 'icon-star' },
];
END;
PROCESS 'helpers/datatables.tt';
-%]
</div>
</div>
</div>
[% END -%]
</div>

@ -0,0 +1,27 @@
[% site_config.title = c.loc('Time Set') -%]
[%
helper.name = c.loc('Time Set');
helper.identifier = 'timeset';
helper.messages = messages;
helper.dt_columns = timeset_dt_columns;
helper.length_change = 1;
helper.close_target = close_target;
helper.create_flag = create_flag;
helper.edit_flag = edit_flag;
helper.form_object = form;
helper.ajax_uri = c.uri_for_action( "/timeset/ajax" );
UNLESS c.user.read_only;
helper.dt_buttons = [
{ name = c.loc('Edit'), uri = "/timeset/'+full.id+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' },
{ name = c.loc('Delete'), uri = "/timeset/'+full.id+'/delete", class = 'btn-small btn-secondary', icon = 'icon-trash' },
];
helper.top_buttons = [
{ name = c.loc('Create Time Set Entry'), uri = c.uri_for('/timeset/create'), icon = 'icon-star' },
];
END;
PROCESS 'helpers/datatables.tt';
-%]
[% # vim: set tabstop=4 syntax=html expandtab: -%]

@ -82,6 +82,7 @@
<li><a href="[% c.uri_for('/lnp') %]">[% c.loc('Number Porting') %]</a></li>
<li><a href="[% c.uri_for('/emergencymapping') %]">[% c.loc('Emergency Mappings') %]</a></li>
<li><a href="[% c.uri_for('/phonebook') %]">[% c.loc('Phonebook') %]</a></li>
<li><a href="[% c.uri_for('/timeset') %]">[% c.loc('Time Sets') %]</a></li>
</ul>
</li>
[% # vim: set tabstop=4 syntax=html expandtab: -%]

@ -0,0 +1,47 @@
use strict;
use warnings;
use Test::Collection;
use Test::FakeData;
use Test::More;
use Data::Dumper;
#init test_machine
my $test_machine = Test::Collection->new(
name => 'timesets',
);
$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)};
my $fake_data = Test::FakeData->new;
$fake_data->set_data_from_script({
timesets => {
data => {
reseller_id => sub { return shift->get_id('resellers',@_); },
name => 'api_test_timeset_name',
times => [{
start => '1971-01-01 00:00:01',
end => '2020-12-31 23:59:59',
until => '1997-01-01 23:59:59',
}],
},
'query' => ['name'],
'data_callbacks' => {
'uniquizer_cb' => sub { Test::FakeData::string_uniquizer(\$_[0]->{name}); },
},
},
});
#for item creation test purposes /post request data/
$test_machine->DATA_ITEM_STORE($fake_data->process('timesets'));
$test_machine->form_data_item( );
# create 3 new sound sets from DATA_ITEM
$test_machine->check_create_correct( 3, sub{ $_[0]->{name} .= $_[1]->{i}; } );
$test_machine->check_get2put();
$test_machine->check_bundle();
$test_machine->clear_test_data_all();
done_testing;
# vim: set tabstop=4 expandtab:
Loading…
Cancel
Save