diff --git a/lib/NGCP/Panel/Controller/API/TimeSets.pm b/lib/NGCP/Panel/Controller/API/TimeSets.pm new file mode 100644 index 0000000000..c5ba9ee233 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/TimeSets.pm @@ -0,0 +1,66 @@ +package NGCP::Panel::Controller::API::TimeSets; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; +use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::TimeSets/; + +use HTTP::Status qw(:constants); + + +sub allowed_methods{ + return [qw/GET POST OPTIONS HEAD/]; +} + +sub api_description { + return 'Defines a collection of (generic) Time Sets, which can each specify a number of ' . + '(recurring) time-slots, which can be currently used in PeeringRules to select certain peerings.'; +} + +sub query_params { + return [ + { + param => 'reseller_id', + description => 'Filter for Time Sets belonging to a specific reseller', + query_type => 'string_eq', + }, + { + param => 'name', + description => 'Filter for items matching a Time Set name pattern', + query_type => 'string_like', + }, + ]; +} + +__PACKAGE__->set_config({ + allowed_roles => [qw/admin reseller/], +}); + +sub create_item { + my ($self, $c, $resource, $form, $process_extras) = @_; + + my $schema = $c->model('DB'); + my $tset; + + 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 }, + }); + } + } catch($e) { + $c->log->error("failed to create timeset: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create timeset."); + return; + } + + return $tset; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/TimeSetsItem.pm b/lib/NGCP/Panel/Controller/API/TimeSetsItem.pm new file mode 100644 index 0000000000..f7afdf4997 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/TimeSetsItem.pm @@ -0,0 +1,32 @@ +package NGCP::Panel::Controller::API::TimeSetsItem; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; +use parent qw/NGCP::Panel::Role::EntitiesItem NGCP::Panel::Role::API::TimeSets/; + +use HTTP::Status qw(:constants); + +sub allowed_methods{ + return [qw/GET OPTIONS HEAD PATCH PUT DELETE/]; +} + +sub journal_query_params { + my($self,$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/]; +} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Field/DateTime.pm b/lib/NGCP/Panel/Field/DateTime.pm index 6133139a9d..de83e12d3d 100644 --- a/lib/NGCP/Panel/Field/DateTime.pm +++ b/lib/NGCP/Panel/Field/DateTime.pm @@ -35,6 +35,10 @@ sub datetime_inflate { # inflate: User entry -> DateTime -> Plaintext but conve } my $date = NGCP::Panel::Utils::DateTime::from_forminput_string($value, $tz); + unless ($date) { + $self->add_error('Could not parse DateTime input. Should be one of (Y-m-d H:M:S, Y-m-d H:M, Y-m-d).'); + return; + } $date->set_time_zone('local'); # convert to local return $date->ymd('-') . ' ' . $date->hms(':'); diff --git a/lib/NGCP/Panel/Field/IntegerList.pm b/lib/NGCP/Panel/Field/IntegerList.pm new file mode 100644 index 0000000000..4dd97e7c03 --- /dev/null +++ b/lib/NGCP/Panel/Field/IntegerList.pm @@ -0,0 +1,40 @@ +package NGCP::Panel::Field::IntegerList; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler::Field::Text'; + +has 'min_value' => (isa => 'Int', default => 0, is => 'rw'); +has 'max_value' => (isa => 'Int', default => 999_999, is => 'rw'); +has 'plusminus' => (isa => 'Bool', default => 0, is => 'rw'); + +sub validate { + my ( $self ) = @_; + my @integers = split(/,/, $self->value); + for my $single_int (@integers) { + $single_int = abs( $single_int ) if $self->plusminus; + if ( !is_int($single_int) ) { + $self->add_error('Value in IntegerList is not numeric.'); + return; + } + if ($single_int < $self->min_value) { + my $min_value = $self->min_value; + $self->add_error("Value in IntegerList ($single_int) is too small (min: $min_value)."); + return; + } + if ($single_int > $self->max_value) { + my $max_value = $self->max_value; + $self->add_error("Value in IntegerList ($single_int) is too big (max: $max_value)."); + return; + } + } + return; +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: + +# describes a list of comma-separated integer numbers (mainly to be used for iCal) +# the integers have to be within the defined range (min_value, max_value) \ No newline at end of file diff --git a/lib/NGCP/Panel/Form/IcalTimeSet.pm b/lib/NGCP/Panel/Form/IcalTimeSet.pm new file mode 100644 index 0000000000..15d3342659 --- /dev/null +++ b/lib/NGCP/Panel/Form/IcalTimeSet.pm @@ -0,0 +1,111 @@ +package NGCP::Panel::Form::IcalTimeSet; +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; + +has_field 'id' => ( + type => 'Hidden', +); + +has_field 'reseller_id' => ( + type => 'Integer', + required => 1, +); + +has_field 'name' => ( + type => 'Text', + label => 'Name', + required => 1, +); + +has_field 'times' => ( + type => 'Repeatable', + do_wrapper => 1, + do_label => 0, + 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::DateTime', + required => 1, +); +has_field 'times.end' => ( + type => '+NGCP::Panel::Field::DateTime', +); +has_field 'times.freq' => ( + type => 'Select', + options => [ + map { +{value => $_, label => $_}; } (qw/secondly minutely hourly daily weekly monthly yearly/) + ], +); +has_field 'times.until' => ( + type => '+NGCP::Panel::Field::DateTime', +); +has_field 'times.count' => ( + type => 'PosInteger', +); +has_field 'times.interval' => ( + type => 'PosInteger', +); +has_field 'times.bysecond' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 0, + max_value => 60, +); +has_field 'times.byminute' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 0, + max_value => 59, +); +has_field 'times.byhour' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 0, + max_value => 60, +); +has_field 'times.byday' => ( + type => 'Text', # (\+|-)?\d*(MO|DI|MI|DO|FR|SA|SU) + # example: 5FR (means fifth friday) +); +has_field 'times.bymonthday' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 1, + max_value => 31, + plusminus => 1, +); +has_field 'times.byyearday' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 1, + max_value => 366, + plusminus => 1, +); +has_field 'times.byweekno' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 1, + max_value => 53, +); +has_field 'times.bymonth' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 1, + max_value => 12, +); +has_field 'times.bysetpos' => ( + type => '+NGCP::Panel::Field::IntegerList', + min_value => 1, + max_value => 366, + plusminus => 1, +); +has_field 'times.comment' => ( + type => 'Text', +); + + + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/TimeSets.pm b/lib/NGCP/Panel/Role/API/TimeSets.pm new file mode 100644 index 0000000000..f0a55a70d2 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/TimeSets.pm @@ -0,0 +1,139 @@ +package NGCP::Panel::Role::API::TimeSets; +use NGCP::Panel::Utils::Generic qw(:all); + +use Sipwise::Base; + +use parent 'NGCP::Panel::Role::API'; + +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use NGCP::Panel::Utils::Subscriber; + +use NGCP::Panel::Form; + +sub resource_name { + return 'timesets'; +} + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::get("NGCP::Panel::Form::IcalTimeSet", $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) : (), + ]; +} + +sub _item_rs { + my ($self, $c) = @_; + my $item_rs; + + if($c->user->roles eq "admin") { + $item_rs = $c->model('DB')->resultset('voip_time_sets'); + } elsif ($c->user->roles eq "reseller") { + my $reseller_id = $c->user->reseller_id; + $item_rs = $c->model('DB')->resultset('voip_time_sets') + ->search_rs({ + 'reseller_id' => $reseller_id, + }); + } + + return $item_rs; +} + +sub resource_from_item { + my ($self, $c, $item, $form) = @_; + + 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; + + return $resource; +} + +# called automatically by POST (and manually by update_item if you want) +sub check_resource { + my($self, $c, $item, $old_resource, $resource, $form) = @_; + + my $schema = $c->model('DB'); + + if(!defined $resource->{reseller_id}) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Missing mandatory field 'reseller_id'"); + return; + } + + my $reseller = $schema->resultset('resellers')->find({ + id => $resource->{reseller_id}, + }); + unless ($reseller) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'reseller_id'."); + return; + } + + if (! exists $resource->{times} ) { + $resource->{times} = []; + } + if (ref $resource->{times} ne "ARRAY") { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'times'. Must be an array."); + return; + } + + return 1; # all good +} + +sub check_duplicate { + my($self, $c, $item, $old_resource, $resource, $form, $process_extras) = @_; + + my $schema = $c->model('DB'); + + my $existing_item = $schema->resultset('voip_time_sets')->find({ + name => $resource->{name}, + }); + if ($existing_item && (!$item || $item->id != $existing_item->id)) { + $c->log->error("time_set name '$$resource{name}' already exists"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "time_set with this name already exists"); + return; + } + return 1; +} + +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 }, + }); + } + $item->discard_changes; + } catch($e) { + $c->log->error("failed to update timeset: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to update timesets."); + return; + }; + + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index a35ce6d4a5..f9a9760332 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -145,6 +145,7 @@ $ua = Test::Collection->new()->ua(); subscriberregistrations => 1, subscribers => 1, systemcontacts => 1, + timesets => 1, topupcash => 1, topuplogs => 1, topupvouchers => 1,