TT#47534 Add iCalendar download/upload web ui

Change-Id: Ib6fa671e8d983ab20ecbe3e7d6e0ce841997211c
changes/99/27299/13
Irina Peshinskaya 7 years ago
parent 345f89d407
commit 9adf470adc

1
debian/control vendored

@ -36,6 +36,7 @@ Depends:
libdata-compare-perl,
libdata-entropy-perl,
libdata-hal-perl,
libdata-ical-perl,
libdata-printer-perl,
libdata-record-perl,
libdata-serializer-perl,

@ -876,12 +876,16 @@ sub timeset_create :Chained('base') :PathPart('timeset/create') :Args(0) :Does(A
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 = {};
my $upload = $c->req->upload('calendarfile');
my $params = {
%{$c->request->params},
upload => $posted ? $upload : undef,
};
$params = merge($params, $c->session->{created_objects});
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
params => $params,
action => $c->uri_for_action('/reseller/timeset_create'),
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
@ -891,6 +895,10 @@ sub timeset_create :Chained('base') :PathPart('timeset/create') :Args(0) :Does(A
if($posted && $form->validated) {
try {
my $resource = $form->values;
$resource = NGCP::Panel::Utils::TimeSet::timeset_resource(
c => $c,
resource => $resource,
);
$resource->{reseller_id} = $reseller->id;
$c->model('DB')->schema->txn_do( sub {
NGCP::Panel::Utils::TimeSet::create_timeset(
@ -958,17 +966,28 @@ sub timeset_edit :Chained('timeset_base') :PathPart('edit') :Args(0) :Does(ACL)
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});
my $upload = $c->req->upload('calendarfile');
my $params = {
%{$c->request->params},
upload => $posted ? $upload : undef,
};
my $item = NGCP::Panel::Utils::TimeSet::get_timeset(c => $c, timeset => $c->stash->{timeset_rs});
$item = merge($item, $c->session->{created_objects});
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
params => $params,
item => $item,
);
if($posted && $form->validated) {
try {
$c->model('DB')->schema->txn_do( sub {
my $resource = $form->values;
$resource = NGCP::Panel::Utils::TimeSet::timeset_resource(
c => $c,
resource => $resource
);
$resource->{reseller_id} = $reseller->id;
NGCP::Panel::Utils::TimeSet::update_timeset(
c => $c,

@ -89,7 +89,11 @@ sub create :Chained('list') :PathPart('create') :Args(0) {
my ($self, $c) = @_;
my $posted = ($c->request->method eq 'POST');
my $params = {};
my $upload = $c->req->upload('calendarfile');
my $params = {
%{$c->request->params},
calendarfile => $posted ? $upload : undef,
};
$params = merge($params, $c->session->{created_objects});
my $form;
if($c->user->roles eq "admin") {
@ -99,8 +103,8 @@ sub create :Chained('list') :PathPart('create') :Args(0) {
}
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
params => $params,
action => $c->uri_for_action('/timeset/create'),
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
@ -112,17 +116,11 @@ sub create :Chained('list') :PathPart('create') :Args(0) {
);
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;
}
my $resource = $c->forward('timeset_resource',[$form]);
$c->model('DB')->schema->txn_do( sub {
NGCP::Panel::Utils::TimeSet::create_timeset(
c => $c,
resource => $form->values,
resource => $resource,
);
});
delete $c->session->{created_objects}->{reseller};
@ -148,9 +146,17 @@ 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 $upload = $c->req->upload('calendarfile');
my $params = {
%{$c->request->params},
calendarfile => $posted ? $upload : undef,
};
my $item = NGCP::Panel::Utils::TimeSet::get_timeset(c => $c, timeset => $c->stash->{timeset_rs});
$item->{reseller}{id} = delete $params->{reseller_id};
$item = merge($item, $c->session->{created_objects});
my $form;
if($c->user->roles eq "admin") {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::Admin", $c);
@ -159,8 +165,8 @@ sub edit :Chained('base') :PathPart('edit') {
}
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
params => $params,
item => $item,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
@ -173,13 +179,7 @@ sub edit :Chained('base') :PathPart('edit') {
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;
}
my $resource = $c->forward('timeset_resource',[$form]);
NGCP::Panel::Utils::TimeSet::update_timeset(
c => $c,
timeset => $c->stash->{timeset_rs},
@ -225,10 +225,39 @@ sub delete_timeset :Chained('base') :PathPart('delete') {
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/timeset'));
}
sub timeset_resource :Private {
my ($self, $c, $form) = @_;
my $resource = $form->values;
return NGCP::Panel::Utils::TimeSet::timeset_resource(
c => $c,
resource => $resource
);
}
sub download :Chained('base') :PathPart('download') :Args(0) {
my ($self, $c) = @_;
my $data_ref = NGCP::Panel::Utils::TimeSet::get_timeset_icalendar(
c => $c,
timeset => $c->stash->{'timeset_rs'},
);
$$data_ref //= '';
my $mime_type = NGCP::Panel::Utils::TimeSet::CALENDAR_MIME_TYPE;
my $extension = mime_type_to_extension($mime_type);
my $filename = NGCP::Panel::Utils::TimeSet::get_calendar_file_name(c => $c, timeset => $c->stash->{'timeset_rs'} ).'.'.$extension;
$c->response->header('Content-Disposition' => 'attachment; filename="'.$filename.'"');
$c->response->content_type($mime_type);
$c->response->body($$data_ref);
return;
}
sub event_list :Chained('base') :PathPart('event') :CaptureArgs(0) {
my ( $self, $c ) = @_;
$c->stash->{events_rs} = $c->model('DB')->resultset('voip_time_periods');
$c->stash->{events_rs} = $c->stash->{timeset_rs}->time_periods;
$c->stash->{event_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => 'id', search => 1, title => $c->loc('#') },
@ -372,4 +401,62 @@ sub event_delete :Chained('event_base') :PathPart('delete') :Args(0) {
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/event'));
}
sub event_upload :Chained('event_list') :PathPart('upload') :Args(0) {
my ($self, $c) = @_;
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::TimeSet::EventUpload", $c);
my $upload = $c->req->upload('calendarfile');
my $posted = $c->req->method eq 'POST';
my @params = ( calendarfile => $posted ? $upload : undef, );
$form->process(
posted => $posted,
params => { @params },
action => $c->uri_for_action('/timeset/event_upload', $c->req->captures),
);
if($form->validated) {
# TODO: check by formhandler?
unless($upload) {
NGCP::Panel::Utils::Message::error(
c => $c,
desc => $c->loc('No iCalendar file specified!'),
);
$c->response->redirect($c->uri_for($c->stash->{timeset}->{id}, 'event'));
return;
}
if ($c->req->params->{purge_existing}) {
$c->stash->{'timeset_rs'}->time_periods->delete;
}
my($events, $fails, $text_success);
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
( $events, $fails, $text_success ) = NGCP::Panel::Utils::TimeSet::parse_calendar_events(c => $c);
NGCP::Panel::Utils::TimeSet::create_timeset_events(
c => $c,
timeset => $c->stash->{'timeset_rs'},
events => $events,
);
});
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $text_success,
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to upload iCalendar events'),
);
}
$c->response->redirect($c->uri_for($c->stash->{timeset}->{id}, 'event'));
return;
}
$c->stash(create_flag => 1);
$c->stash(form => $form);
}
1;

@ -15,7 +15,7 @@ has_field 'reseller' => (
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/id reseller name/],
render_list => [qw/id reseller name calendarfile/],
);
1;

@ -0,0 +1,69 @@
package NGCP::Panel::Form::TimeSet::EventUpload;
use HTML::FormHandler::Moose;
use HTML::FormHandler::Widget::Block::Bootstrap;
use NGCP::Panel::Utils::Form;
extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
has '+widget_wrapper' => ( default => 'Bootstrap' );
has '+enctype' => ( default => 'multipart/form-data');
has_field 'submitid' => ( type => 'Hidden' );
sub build_render_list {[qw/submitid fields actions/]}
sub build_form_element_class { [qw/form-horizontal/] }
has_field 'calendarfile' => (
type => 'Upload',
max_size => '67108864', # 64MB
);
has_field 'purge_existing' => (
type => 'Boolean',
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/calendarfile purge_existing /],
);
has_field 'save' => (
type => 'Submit',
value => 'Save',
element_class => [qw/btn btn-primary/],
label => '',
);
has_block 'actions' => (
tag => 'div',
class => [qw/modal-footer/],
render_list => [qw/save/],
);
1;
__END__
=head1 NAME
NGCP::Panel::Form::TimeSet::EventUpload
=head1 DESCRIPTION
Preferences Form.
=head1 METHODS
=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:

@ -6,11 +6,15 @@ extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
with 'NGCP::Panel::Render::RepeatableJs';
has '+enctype' => ( default => 'multipart/form-data');
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/] }
use NGCP::Panel::Utils::TimeSet;
has_field 'id' => (
type => 'Hidden',
);
@ -18,9 +22,24 @@ has_field 'id' => (
has_field 'name' => (
type => 'Text',
label => 'Name',
required => 1,
required => 0,
validate_when_empty => 1,
label_attr => {
rel => ['tooltip'],
title => ['Name should be specified in the input field or in the uploaded calendar file. Name from the form input has priority.']
},
);
has_field 'calendarfile' => (
type => 'Upload',
max_size => '67108864', # 64MB
);
#has_field 'purge_existing' => (
# type => 'Boolean',
# label => 'Purge existing events',
#);
has_field 'save' => (
type => 'Submit',
value => 'Save',
@ -31,7 +50,7 @@ has_field 'save' => (
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/id name/],
render_list => [qw/id name calendarfile/],
);
has_block 'actions' => (
@ -40,13 +59,25 @@ has_block 'actions' => (
render_list => [qw/save/],
);
#override 'update_fields' => sub {
# my $self = shift;
# my $c = $self->ctx;
# return unless $c;
#
# super();
# if (!$c->stash->{timeset_rs}) {
# $self->field('purge_existing')->inactive(1);
# } else {
# $self->field('purge_existing')->inactive(0);
# }
#};
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') {
@ -64,11 +95,30 @@ sub validate {
$self->field('name')->add_error($c->loc('Unknow reseller'));
}
#/todo
my $name = $self->field('name')->value;
if (!$name) {
my $timeset_uploaded = {};
if ($self->field('calendarfile')->value) {
($timeset_uploaded) = NGCP::Panel::Utils::TimeSet::parse_calendar( c => $c );
}
if (!$timeset_uploaded->{name}) {
$self->field('name')->add_error($c->loc('Name field is required and should be defined in the form field or in the uploaded calendar file.'));
} else {
$name = $timeset_uploaded->{name};
}
}
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)) {
my $current_item = $self->item ? $self->item : $c->stash->{timeset_rs};
my $current_item_id =
$current_item && $c->request->path !~ /\/copy\//
? ref $current_item eq 'HASH'
? $current_item->{id} : $current_item->id
: undef;
if ($existing_item && (!$current_item_id || $existing_item->id != $current_item_id)) {
$self->field('name')->add_error($c->loc('This name already exists'));
}
}

@ -16,9 +16,10 @@ use Data::Compare qw//;
my $MIME_TYPES = {
#first extension is default, others are for extension 2 mime_type detection
'audio/x-wav' => ['wav'],
'audio/mpeg' => ['mp3'],
'audio/ogg' => ['ogg'],
'audio/x-wav' => ['wav'],
'audio/mpeg' => ['mp3'],
'audio/ogg' => ['ogg'],
'text/calendar' => ['ics'],
};
sub is_int {

@ -5,6 +5,15 @@ use warnings;
use Sipwise::Base;
use NGCP::Panel::Utils::DateTime;
use Data::ICal;
use constant CALENDAR_MIME_TYPE => 'text/calendar';
sub get_calendar_file_name {
my %params = @_;
my($c, $timeset) = @params{qw/c timeset/};
return $timeset->name.'_'.$timeset->id;
}
sub delete_timeset {
my %params = @_;
@ -21,6 +30,7 @@ sub update_timeset {
name => $resource->{name},
reseller_id => $resource->{reseller_id},
})->discard_changes;
$timeset->time_periods->delete;
create_timeset_events(
c => $c,
@ -82,6 +92,171 @@ sub get_timeset {
$resource->{times} = \@periods;
return $resource;
}
sub get_timeset_icalendar {
my %params = @_;
my($c, $timeset) = @params{qw/c timeset/};
my $data = '';
my $data_ref = $timeset->timeset_ical ? \$timeset->timeset_ical->ical : \$data;
return $data_ref;
}
sub timeset_resource {
my (%params) = @_;
my $c = $params{c};
my $api_format = $params{api_format};
my $resource = $params{resource}
// ( $params{form} ? $params{form}->values : {});
delete $resource->{calendarfile};
if($c->user->roles eq 'admin') {
if ($resource->{reseller}) {
if ( !$resource->{reseller_id} ) {
$resource->{reseller_id} = $resource->{reseller}{id};
}
delete $resource->{reseller};
}
} elsif($c->user->roles eq 'reseller') {
$resource->{reseller_id} = $c->user->reseller_id;
}
if (!$resource->{name}) {
my( $calendar_parsed ) = NGCP::Panel::Utils::TimeSet::parse_calendar(
c => $c,
);
#we have checked that $name is not empty in the form validation
$resource->{name} = $calendar_parsed->{name};
}
#data will be taken from the request parameters or cache
my($fails, $text_success);
($resource->{times}, $fails, $text_success) = NGCP::Panel::Utils::TimeSet::parse_calendar_events(c => $c);
return $resource;
}
sub get_calendar_data_parsed {
my %params = @_;
my($c, $data) = @params{qw/c data/};
use Carp qw/longmess/;
$c->log->debug(longmess);
if ($c->stash->{calendar_upload_parsed}) {
return $c->stash->{calendar_upload_parsed};
}
if (!$data && $c->req->upload('calendarfile')) {
$data = \$c->req->upload('calendarfile')->slurp;
$data //= '';
$$data =~s/\n+/\n/g;
$c->stash(
calendar_upload => $data,
);
}
if (!$data) {
return;
}
$c->log->debug("calendar data: ".$$data.";");
my $calendar = Data::ICal->new( data => $$data );
if (!$calendar) {
#https://metacpan.org/pod/Data::ICal
#parse [ data => $data, ] [ filename => $file, ]
#Returns $self on success. Returns a false value upon failure to open or parse the file or data; this false value is a Class::ReturnValue object and can be queried as to its error_message.
$c->log->debug("calendar error messages: ".$calendar->error_message.";");
} else {
$c->stash(
calendar_upload_parsed => $calendar,
);
}
return $calendar, $data;
}
sub parse_calendar{
my %params = @_;
my($c, $data) = @params{qw/c data/};
my $timeset = {};
#we will use caching because we need to parse uploaded fie to check name existence and uniqueness
if ($c->stash->{calendar_upload_parsed_result}) {
return $c->stash->{calendar_upload_parsed_result}, $c->stash->{calendar_upload_parsed};
}
my ($calendar) = get_calendar_data_parsed( c=> $c, data => $data );
if ($calendar) {
if ($calendar->property('name')) {
$timeset->{name} = $calendar->property('name')->[0]->value;
}
}
$c->stash(
calendar_upload_parsed => $calendar,
calendar_upload_parsed_result => $timeset,
);
return $timeset, $calendar;
}
sub parse_calendar_events {
my %params = @_;
my($c, $calendar, $data, $api_format) = @params{qw/c calendar data api_format/};
my ($events, $fails, $text_success) = ([], undef, 'Calendar events successfully uploaded');
if(!$calendar) {
if (!$c->stash->{calendar_upload_parsed}) {
($calendar) = get_calendar_data_parsed( c=> $c, data => $data );
}
}
if ($calendar) {
$c->log->debug("parse calendar events;");
my @allowed_rrule_fields = (qw/FREQ COUNT UNTIL INTERVAL BYSECOND BYMINUTE BYHOUR BYDAY BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS WKST SUMMARY/);
my @all_rrule_fields = (@allowed_rrule_fields, qw/RDATE EXDATE/);
my %rrule_fields_end_markers = map {
my $field = $_;
$field => join('|', grep {$_ ne $field} @all_rrule_fields)
} @all_rrule_fields;
#or:
#my $rrule_fields_end_marker = '[a-z]+';
my @datetime_fields = qw/dtstart dtend until/;
my $mapped_fields = {
'dtstart' => 'start',
'dtend' => 'end',
'summary' => 'comment',
};
foreach my $entry (@{$calendar->entries}) {
$c->log->debug("parse calendar entry:".$entry->as_string .";");
my $event = {
map {
$entry->property($_)
? ( $_ => $entry->property($_)->[0]->value )
: ()
} qw/summary dtstart dtend/
};
my $rrule = $entry->property('rrule');
if ( ref $rrule eq 'ARRAY' && @$rrule ) {
#we don't expect some RRULE spec in one event for now
my $rrule_data = { map {
my $FIELD = $_;
my $field = lc($FIELD);
my $re = '(?:^|;)'.$FIELD.'=(.*?)(?:;(?:'.$rrule_fields_end_markers{$FIELD}.')|;$|$)';
$rrule->[0]->value =~/$re/i;
my $value = $1;
if ($value) {
($field => $value);
} else {
();
}
} @allowed_rrule_fields };
$event = {%$event, %$rrule_data};
foreach my $dt_field (@datetime_fields) {
if ($event->{$dt_field}) {
$event->{$dt_field} =~s/^\s*(\d{4})\D*(\d{2})\D*(\d{2})(\D*)(\d{2})\D*(\d{2})\D*(\d{2})(\D*?)\s*$/$1-$2-$3$4$5:$6:$7$8/;
}
}
foreach my $ical_field (keys %$mapped_fields) {
if ($event->{$ical_field}) {
$event->{$mapped_fields->{$ical_field}} = delete $event->{$ical_field};
}
}
}
push @$events, $event;
}
}
return $events, $fails, $text_success;
}
1;
=head1 NAME

@ -461,6 +461,7 @@
UNLESS c.user.read_only;
helper.dt_buttons = [
{ name = c.loc('Events'), uri = "/timeset/'+full.id+'/event", class = 'btn-small btn-tertiary', icon = 'icon-th-list' },
{ name = c.loc('Download'), uri = "/timeset/'+full.id+'/download", class = 'btn-small btn-primary', icon = 'icon-th-list' },
{ 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' },
];

@ -213,6 +213,7 @@ $( document ).ready(function() {
];
helper.top_buttons = [
{ name = c.loc('Create Event'), uri = c.uri_for_action('/timeset/event_create', [c.req.captures.0] ), icon = 'icon-star' },
{ name = c.loc('Upload iCalendar events'), uri = c.uri_for_action('/timeset/event_upload',[c.req.captures.0]), icon = 'icon-star' },
];
END;

@ -15,6 +15,7 @@
UNLESS c.user.read_only;
helper.dt_buttons = [
{ name = c.loc('Events'), uri = "/timeset/'+full.id+'/event", class = 'btn-small btn-tertiary', icon = 'icon-th-list' },
{ name = c.loc('Download'), uri = "/timeset/'+full.id+'/download", class = 'btn-small btn-primary', icon = 'icon-th-list' },
{ 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' },
];

Loading…
Cancel
Save