TT#14755 call list suppressions

Change-Id: Ie1a75f5ee2465e68d62d6ad4f77c6a0d5bd729b6
changes/14/14014/9
Rene Krenn 8 years ago
parent eec8dd34e0
commit d6632b5178

@ -13,6 +13,7 @@ use NGCP::Panel::Utils::DateTime;
use Path::Tiny qw(path);
use Safe::Isa qw($_isa);
use DateTime::TimeZone;
use NGCP::Panel::Utils::CallList qw();
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::CheckTrailingSlash;
require NGCP::Panel::Role::HTTPMethods;
@ -37,15 +38,17 @@ sub query_params {
description => 'Filter for calls for a specific subscriber. Either this or customer_id is mandatory if called by admin, reseller or subscriberadmin to filter list down to a specific subscriber in order to properly determine the direction of calls.',
new_rs => sub {
my ($c,$q,$rs) = @_;
my $subscriber = $c->model('DB')->resultset('voip_subscribers')->find($q);
if ($subscriber) {
my $out_rs = $rs->search_rs({
source_user_id => $subscriber->uuid,
});
my $in_rs = $rs->search_rs({
destination_user_id => $subscriber->uuid,
});
return $out_rs->union_all($in_rs);
if ($c->user->roles ne "subscriber") {
my $subscriber = $c->model('DB')->resultset('voip_subscribers')->find($q);
if ($subscriber) {
my $out_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$rs->search_rs({
source_user_id => $subscriber->uuid,
}),NGCP::Panel::Utils::CallList::SUPPRESS_OUT);
my $in_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$rs->search_rs({
destination_user_id => $subscriber->uuid,
}),NGCP::Panel::Utils::CallList::SUPPRESS_IN);
return $out_rs->union_all($in_rs);
}
}
return $rs;
},
@ -53,17 +56,17 @@ sub query_params {
{
param => 'customer_id',
description => 'Filter for calls for a specific customer. Either this or subscriber_id is mandatory if called by admin, reseller or subscriberadmin to filter list down to a specific customer. For calls within the same customer_id, the direction will always be "out".',
query => {
first => sub {
my $q = shift;
return {
-or => [
'source_account_id' => $q,
'destination_account_id' => $q,
],
};
},
second => sub {},
new_rs => sub {
my ($c,$q,$rs) = @_;
if ($c->user->roles ne "subscriber" and $c->user->roles ne "subscriberadmin" and not exists $c->req->query_params->{subscriber_id}) {
return NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$rs->search_rs({
-or => [
'source_account_id' => $q,
'destination_account_id' => $q,
],
},undef),NGCP::Panel::Utils::CallList::SUPPRESS_INOUT);
}
return $rs;
},
},
{
@ -224,35 +227,39 @@ sub query_params {
},
{
param => 'own_cli',
description => 'Filter calls by a specific number that is a part of in our out calls.',
new_rs => sub {
my ($c,$q,$rs) = @_;
my $owner = $c->stash->{owner} // {};
return unless $owner;
if ($owner->{subscriber}) {
return $rs->search_rs({
-or => [
{ source_cli => $q,
source_user_id => $owner->{subscriber}->uuid,
},
{ destination_user_in => $q,
destination_user_id => $owner->{subscriber}->uuid,
},
],
});
} elsif ($owner->{customer}) {
return $rs->search_rs({
-or => [
{ source_cli => $q,
source_account_id => $owner->{customer}->id,
},
{ destination_user_in => $q,
destination_account_id => $owner->{customer}->id,
},
],
});
}
description => 'Filter calls by a specific number that is a part of in or out calls.',
query => {
first => sub {
my ($q,$c) = @_;
my $owner = $c->stash->{owner} // {};
return unless $owner;
if ($owner->{subscriber}) {
return {
-or => [
{ source_cli => $q,
source_user_id => $owner->{subscriber}->uuid,
},
{ destination_user_in => $q,
destination_user_id => $owner->{subscriber}->uuid,
},
],
};
} elsif ($owner->{customer}) {
return {
-or => [
{ source_cli => $q,
source_account_id => $owner->{customer}->id,
},
{ destination_user_in => $q,
destination_account_id => $owner->{customer}->id,
},
],
};
}
},
second => sub {},
},
},
];
}

@ -0,0 +1,295 @@
package NGCP::Panel::Controller::CallListSuppression;
use NGCP::Panel::Utils::Generic qw(:all);
use Sipwise::Base;
use parent 'Catalyst::Controller';
use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Datatables;
use NGCP::Panel::Utils::MySQL;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::CallList qw();
use NGCP::Panel::Form::CallListSuppression::Suppression;
use NGCP::Panel::Form::CallListSuppression::Upload;
sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) {
my ($self, $c) = @_;
$c->log->debug(__PACKAGE__ . '::auto');
NGCP::Panel::Utils::Navigation::check_redirect_chain(c => $c);
return 1;
}
sub list :Chained('/') :PathPart('calllistsuppression') :CaptureArgs(0) {
my ( $self, $c ) = @_;
my $rs = $c->model('DB')->resultset('call_list_suppressions');
$c->stash(rs => $rs);
$c->stash->{calllistsuppression_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", "search" => 1, "title" => $c->loc("#") },
{ name => "domain", "search" => 1, "title" => $c->loc("Domain") },
{ name => "direction", "search" => 1, "title" => $c->loc("Direction") },
{ name => "pattern", "search" => 1, "title" => $c->loc("Pattern") },
{ name => "mode", "search" => 1, "title" => $c->loc("Mode") },
{ name => "label", "search" => 1, "title" => $c->loc("Label") },
]);
$c->stash(template => 'calllistsuppression/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->{rs};
NGCP::Panel::Utils::Datatables::process($c, $rs, $c->stash->{calllistsuppression_dt_columns});
$c->detach( $c->view("JSON") );
}
sub base :Chained('list') :PathPart('') :CaptureArgs(1) {
my ($self, $c, $sup_id) = @_;
unless($sup_id && is_int($sup_id)) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Invalid call list suppression id detected',
desc => $c->loc('Invalid call list suppression id detected'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/calllistsuppression'));
}
my $res = $c->stash->{rs}->find($sup_id);
unless(defined($res)) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Call list suppression does not exist',
desc => $c->loc('Call list suppression does not exist'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/calllistsuppression'));
}
$c->stash(sup => $res);
}
sub edit :Chained('base') :PathPart('edit') {
my ($self, $c ) = @_;
my $form;
my $sup = $c->stash->{sup};
my $posted = ($c->request->method eq 'POST');
my $params = { $sup->get_inflated_columns };
$params = merge($params, $c->session->{created_objects});
$form = NGCP::Panel::Form::CallListSuppression::Suppression->new(ctx => $c);
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
fields => {},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
my $dup_item = $schema->resultset('call_list_suppressions')->find({
domain => $form->values->{domain},
pattern => $form->values->{pattern},
direction => $form->values->{direction},
});
if($dup_item && $dup_item->id != $sup->id) {
die( ["The combination of domain, direction and pattern should be unique", "showdetails"] );
}
$sup->update($form->values);
#delete $c->session->{created_objects}->{reseller};
});
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Call list suppression successfully updated'),
);
$c->flash(messages => delete $c->flash->{messages});
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to update call list suppression'),
);
$c->flash(messages => delete $c->flash->{messages});
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/calllistsuppression'));
}
$c->stash(edit_flag => 1);
$c->stash(form => $form);
}
sub create :Chained('list') :PathPart('create') :Args(0) {
my ($self, $c) = @_;
my $schema = $c->model('DB');
my $posted = ($c->request->method eq 'POST');
my $form;
my $params = {};
$params = merge($params, $c->session->{created_objects});
$form = NGCP::Panel::Form::CallListSuppression::Suppression->new(ctx => $c);
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
fields => {},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
my $dup_item = $schema->resultset('call_list_suppressions')->find({
domain => $form->values->{domain},
pattern => $form->values->{pattern},
direction => $form->values->{direction},
});
if($dup_item) {
die( ["The combination of domain, direction and pattern already exists", "showdetails"] );
}
my $sup = $c->model('DB')->resultset('call_list_suppressions')->create($form->values);
$c->session->{created_objects}->{call_list_suppression} = { id => $sup->id };
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Call list suppression successfully created'),
);
$c->flash(messages => delete $c->flash->{messages});
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to create call list suppression'),
);
$c->flash(messages => delete $c->flash->{messages});
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/calllistsuppression'));
}
$c->stash(create_flag => 1);
$c->stash(form => $form);
}
sub delete :Chained('base') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
try {
my $schema = $c->model('DB');
$schema->txn_do(sub{
$c->stash->{sup}->delete;
});
NGCP::Panel::Utils::Message::info(
c => $c,
data => { $c->stash->{sup}->get_inflated_columns },
desc => $c->loc('Call list suppression successfully deleted'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to delete call list suppression.'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/calllistsuppression'));
}
sub upload :Chained('list') :PathPart('upload') :Args(0) {
my ($self, $c) = @_;
my $form = NGCP::Panel::Form::CallListSuppression::Upload->new(ctx => $c);
my $upload = $c->req->upload('upload_calllistsuppression');
my $posted = $c->req->method eq 'POST';
my @params = ( upload_lnp => $posted ? $upload : undef, );
$form->process(
posted => $posted,
params => { @params },
action => $c->uri_for('/calllistsuppression/upload'),
);
if($form->validated) {
# TODO: check by formhandler?
unless($upload) {
NGCP::Panel::Utils::Message::error(
c => $c,
desc => $c->loc('No call list suppression file specified!'),
);
$c->flash(messages => delete $c->flash->{messages});
$c->response->redirect($c->uri_for('/calllistsuppression'));
return;
}
my $data = $upload->slurp;
my($fails, $text_success);
try {
my $schema = $c->model('DB');
$schema->txn_do(sub {
if($c->req->params->{purge_existing}) {
my ($start, $end);
$start = time;
NGCP::Panel::Utils::MySQL::truncate_table(
c => $c,
schema => $schema,
do_transaction => 0,
table => 'billing.call_list_suppressions',
);
$c->stash->{rs}->delete;
$end = time;
$c->log->debug("Purging call list suppressions took " . ($end - $start) . "s");
}
( $fails, $text_success ) = NGCP::Panel::Utils::CallList::upload_suppressions_csv(
c => $c,
data => \$data,
schema => $schema,
);
});
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $$text_success,
);
$c->flash(messages => delete $c->flash->{messages});
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to upload call list suppressions'),
);
$c->flash(messages => delete $c->flash->{messages});
}
$c->response->redirect($c->uri_for('/calllistsuppression'));
return;
}
$c->stash(create_flag => 1);
$c->stash(form => $form);
}
sub download :Chained('list') :PathPart('download') :Args(0) {
my ($self, $c) = @_;
my $schema = $c->model('DB');
$c->response->header ('Content-Disposition' => 'attachment; filename="call_list_suppressions.csv"');
$c->response->content_type('text/csv');
$c->response->status(200);
NGCP::Panel::Utils::CallList::create_suppressions_csv(
c => $c,
);
return;
}
__PACKAGE__->meta->make_immutable;
1;

@ -9,6 +9,7 @@ use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::InvoiceTemplate;
use NGCP::Panel::Utils::Invoice;
use NGCP::Panel::Utils::CallList qw();
use NGCP::Panel::Form::Invoice::Invoice;
use HTML::Entities;
@ -253,7 +254,7 @@ sub create :Chained('inv_list') :PathPart('create') :Args() :Does(ACL) :ACLDetac
$call->{destination_user_in} =~s/%23/#/g;
#$call->{destination_user_in} = encode_entities($call->{destination_user_in}, '<>&"#');
$call->{source_customer_cost} += 0.0; # make sure it's a number
$call;
NGCP::Panel::Utils::CallList::suppress_cdr_fields($c,$call,$_);
} $calllist_rs->all ];
#my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));

@ -3585,14 +3585,14 @@ sub _process_calls_rows {
sub ajax_calls :Chained('calllist_master') :PathPart('list/ajax') :Args(0) {
my ($self, $c) = @_;
my $callid = $c->stash->{callid};
my $out_rs = $c->model('DB')->resultset('cdr')->search({
my $out_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search({
source_user_id => $c->stash->{subscriber}->uuid,
($callid ? (call_id => $callid) : ()),
});
my $in_rs = $c->model('DB')->resultset('cdr')->search({
}),NGCP::Panel::Utils::CallList::SUPPRESS_OUT);
my $in_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search({
destination_user_id => $c->stash->{subscriber}->uuid,
($callid ? (call_id => $callid) : ()),
});
}),NGCP::Panel::Utils::CallList::SUPPRESS_IN);
my $rs = $out_rs->union_all($in_rs);
_process_calls_rows($c,$rs);
@ -3603,9 +3603,9 @@ sub ajax_calls :Chained('calllist_master') :PathPart('list/ajax') :Args(0) {
sub ajax_calls_in :Chained('calllist_master') :PathPart('list/ajax/in') :Args(0) {
my ($self, $c) = @_;
my $rs = $c->model('DB')->resultset('cdr')->search({
my $rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search({
destination_user_id => $c->stash->{subscriber}->uuid,
});
}),NGCP::Panel::Utils::CallList::SUPPRESS_IN);
_process_calls_rows($c,$rs);
@ -3615,9 +3615,9 @@ sub ajax_calls_in :Chained('calllist_master') :PathPart('list/ajax/in') :Args(0)
sub ajax_calls_out :Chained('calllist_master') :PathPart('list/ajax/out') :Args(0) {
my ($self, $c) = @_;
my $rs = $c->model('DB')->resultset('cdr')->search({
my $rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search({
source_user_id => $c->stash->{subscriber}->uuid,
});
}),NGCP::Panel::Utils::CallList::SUPPRESS_OUT);
_process_calls_rows($c,$rs);

@ -0,0 +1,94 @@
package NGCP::Panel::Form::CallListSuppression::Suppression;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
use NGCP::Panel::Utils::Form;
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 'domain' => (
type => 'Text',
label => 'Domain',
required => 0,
element_attr => {
rel => ['tooltip'],
title => ['The domain of subscribers, this call list suppression applies to. An empty domain means to apply it to subscribers of any domain.']
},
);
has_field 'direction' => (
type => 'Select',
label => 'Direction',
options => [
{ value => 'outgoing', label => 'outgoing' },
{ value => 'incoming', label => 'incoming' },
],
element_attr => {
rel => ['tooltip'],
title => ['The direction of calls this call list suppression applies to.']
},
required => 1,
);
has_field 'pattern' => (
type => 'Text',
label => 'Pattern',
required => 1,
element_attr => {
rel => ['tooltip'],
title => ['A regular expression the dialed number (CDR \'destination user in\') has to match in case of \'outgoing\' direction, or the inbound number (CDR \'source cli\') in case of \'incoming\' direction.']
},
);
has_field 'mode' => (
type => 'Select',
label => 'Mode',
options => [
{ value => 'filter', label => 'filter' },
{ value => 'obfuscate', label => 'obfuscate' },
{ value => 'disabled', label => 'disabled' },
],
element_attr => {
rel => ['tooltip'],
title => ['The suppression mode. For subscriber and subscriber admins, filtering means matching calls are not listed at all, while obfuscation means the number is replaced by the given label.']
},
required => 1,
);
has_field 'label' => (
type => 'Text',
label => 'Label',
required => 1,
element_attr => {
rel => ['tooltip'],
title => ['The replacement string in case of obfuscation mode. Admin and reseller users see it for filter mode suppressions.']
},
);
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/domain direction pattern mode label/ ],
);
has_block 'actions' => (
tag => 'div',
class => [qw/modal-footer/],
render_list => [qw/save/],
);
1;

@ -0,0 +1,49 @@
package NGCP::Panel::Form::CallListSuppression::Suppression;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
has_field 'id' => (
type => 'Hidden'
);
has_field 'domain' => (
type => 'Text',
label => 'The domain of subscribers, this call list suppression applies to. An empty domain means to apply it to subscribers of any domain.',
required => 0,
);
has_field 'direction' => (
type => 'Select',
label => 'The direction of calls this call list suppression applies to.',
options => [
{ value => 'outgoing', label => 'outgoing' },
{ value => 'incoming', label => 'incoming' },
],
required => 1,
);
has_field 'pattern' => (
type => 'Text',
label => 'A regular expression the dialed number (CDR \'destination user in\') has to match in case of \'outgoing\' direction, or the inbound number (CDR \'source cli\') in case of \'incoming\' direction.',
required => 1,
);
has_field 'mode' => (
type => 'Select',
label => 'The suppression mode. For subscriber and subscriber admins, filtering means matching calls do not appear at all, while obfuscation means the number is replaced by the given label.',
options => [
{ value => 'filter', label => 'filter' },
{ value => 'obfuscate', label => 'obfuscate' },
{ value => 'disabled', label => 'disabled' },
],
required => 1,
);
has_field 'label' => (
type => 'Text',
label => 'The replacement string in case of obfuscation mode. Admin and reseller users see it for filter mode suppressions.',
required => 1,
);
1;

@ -0,0 +1,44 @@
package NGCP::Panel::Form::CallListSuppression::Upload;
use Sipwise::Base;
use HTML::FormHandler::Moose;
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 'upload_calllistsuppression' => (
type => 'Upload',
max_size => '2097152000', # 2GB
);
has_field 'purge_existing' => (
type => 'Boolean',
);
has_field 'save' => (
type => 'Submit',
value => 'Upload',
element_class => [qw/btn btn-primary/],
do_label => 0,
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/upload_calllistsuppression purge_existing/],
);
has_block 'actions' => (
tag => 'div',
class => [qw/modal-footer/],
render_list => [qw/save/],
);
1;
# vim: set tabstop=4 expandtab:

@ -740,7 +740,7 @@ sub apply_query_params {
return $item_rs;
}
foreach my $param(keys %{ $c->req->query_params }) {
foreach my $param(_get_sorted_query_params($c,$query_params)) {
my @p = grep { $_->{param} eq $param } @{ $query_params };
#todo: we can generate default filters for all item_rs fields here
#the only reason not to do this is a security
@ -759,9 +759,30 @@ sub apply_query_params {
}
}
}
#use DBIx::Class::Helper::ResultSet::Explain qw();
#use Data::Dumper;
#$c->log->debug(Dumper(DBIx::Class::Helper::ResultSet::Explain::explain($item_rs)));
return $item_rs;
}
sub _get_sorted_query_params {
my ($c,$query_params) = @_;
#use Data::Dumper;
#$c->log->debug('request params: ' . Dumper($c->req->query_params));
#$c->log->debug('request param keys: ' . Dumper(keys %{$c->req->query_params}));
#$c->log->debug('supported filters: ' . Dumper($query_params));
my %query_params_map = map { $_->{param} => $_; } @$query_params;
#$c->log->debug('supported filter map: ' . Dumper(\%query_params_map));
my @sorted = sort {
(exists $query_params_map{$a} and exists $query_params_map{$a}->{new_rs}) <=> (exists $query_params_map{$b} and exists $query_params_map{$b}->{new_rs});
} keys %{$c->req->query_params};
#$c->log->debug('request params: ' . Dumper($c->req->query_params));
#$c->log->debug('request params sorted: ' . Dumper(\@sorted));
return @sorted;
}
sub get_query_callbacks{
my ($self, $query_param_spec) = @_;
#while believe that there is only one parameter

@ -15,6 +15,7 @@ use DateTime::Format::Strptime;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::CallList;
use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Utils::CallList qw();
use NGCP::Panel::Form::CallList::Subscriber;
sub _item_rs {
@ -33,25 +34,31 @@ sub _item_rs {
} elsif($c->user->roles eq "subscriberadmin") {
$item_rs = $item_rs->search({
-or => [
{ 'source_account_id' => $c->user->account_id },
{ 'destination_account_id' => $c->user->account_id },
{ source_account_id => $c->user->account_id },
{ destination_account_id => $c->user->account_id },
],
});
if (not exists $c->req->query_params->{subscriber_id}) {
$item_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$item_rs,NGCP::Panel::Utils::CallList::SUPPRESS_INOUT);
}
} else {
my $out_rs = $item_rs->search_rs({
my $out_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$item_rs->search_rs({
source_user_id => $c->user->voip_subscriber->uuid,
});
my $in_rs = $item_rs->search_rs({
}),NGCP::Panel::Utils::CallList::SUPPRESS_OUT);
my $in_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$item_rs->search_rs({
destination_user_id => $c->user->voip_subscriber->uuid,
});
}),NGCP::Panel::Utils::CallList::SUPPRESS_IN);
$item_rs = $out_rs->union_all($in_rs);
}
$item_rs = $item_rs->search({
-not => [
{ 'destination_domain_in' => 'vsc.local' },
],
});
return $item_rs;
}
sub get_form {

@ -4,9 +4,21 @@ use strict;
use warnings;
use JSON qw();
use Scalar::Util;
use Text::CSV_XS;
use NGCP::Panel::Utils::MySQL;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Subscriber;
use constant SUPPRESS_OUT => 1;
use constant SUPPRESS_IN => 2;
use constant SUPPRESS_INOUT => 3;
use constant SOURCE_CLI_SUPPRESSION_ID_COLNAME => 'source_cli_suppression_id';
use constant DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME => 'destination_user_in_suppression_id';
use constant ENABLE_SUPPRESSIONS => 1; #setting to 0 totally disables call list suppressions -> no discussion about performance difference.
# owner:
# * subscriber (optional)
# * customer
@ -27,6 +39,7 @@ use NGCP::Panel::Utils::Subscriber;
# * rating_status
# * type
sub process_cdr_item {
my ($c, $item, $owner, $params) = @_;
my $sub = $owner->{subscriber};
@ -70,6 +83,8 @@ sub process_cdr_item {
$source_cli = $anonymize ? 'anonymous@anonymous.invalid' : $source_cli;
$resource->{clir} = $item->source_clir;
my ($source_cli_suppression,$destination_user_in_suppression) = _get_suppressions($c,$item);
my ($src_sub, $dst_sub);
my $billing_src_sub = $item->source_subscriber;
my $billing_dst_sub = $item->destination_subscriber;
@ -79,7 +94,7 @@ sub process_cdr_item {
if($billing_dst_sub && $billing_dst_sub->provisioning_voip_subscriber) {
$dst_sub = $billing_dst_sub->provisioning_voip_subscriber;
}
my ($own_normalize, $other_normalize, $own_domain, $other_domain);
my ($own_normalize, $other_normalize, $own_domain, $other_domain, $own_suppression, $other_suppression);
my $other_skip_domain = 0;
if($resource->{direction} eq "out") {
@ -101,6 +116,7 @@ sub process_cdr_item {
$own_normalize = 1;
}
$own_domain = $item->source_domain;
$own_suppression = $source_cli_suppression;
# for intra pbx out calls, use extension as other cli
if($intra && $dst_sub && $dst_sub->pbx_extension) {
@ -120,6 +136,7 @@ sub process_cdr_item {
$other_normalize = 1;
}
$other_domain = $item->destination_domain;
$other_suppression = $destination_user_in_suppression;
} else {
# for pbx in calls, use extension as own cli
if($dst_sub && $dst_sub->pbx_extension) {
@ -139,6 +156,7 @@ sub process_cdr_item {
$own_normalize = 1;
}
$own_domain = $item->destination_domain;
$own_suppression = $destination_user_in_suppression;
# rewrite cf to voicemail to "voicemail"
if($item->destination_user_in =~ /^vm[ub]/ &&
@ -161,22 +179,25 @@ sub process_cdr_item {
$other_normalize = 0;
$other_skip_domain = 1;
$resource->{direction} = "out";
# for intra pbx in calls, use extension as other cli
} elsif($intra && $src_sub && $src_sub->pbx_extension) {
$resource->{other_cli} = $src_sub->pbx_extension;
# for termianted subscribers if there is an alias field (e.g. gpp0), use this
} elsif($intra && $item->source_account_id && $params->{'intra_alias_field'}) {
my $alias = $item->get_column('source_'.$params->{'intra_alias_field'});
$resource->{other_cli} = $alias // $source_cli;
$other_normalize = 0;
# if there is an alias field (e.g. gpp0), use this
} elsif($item->source_account_id && $params->{'alias_field'}) {
my $alias = $item->get_column('source_'.$params->{'alias_field'});
$resource->{other_cli} = $alias // $source_cli;
$other_normalize = 1;
} else {
$resource->{other_cli} = $source_cli;
$other_normalize = 1;
# for intra pbx in calls, use extension as other cli
if($intra && $src_sub && $src_sub->pbx_extension) {
$resource->{other_cli} = $src_sub->pbx_extension;
# for termianted subscribers if there is an alias field (e.g. gpp0), use this
} elsif($intra && $item->source_account_id && $params->{'intra_alias_field'}) {
my $alias = $item->get_column('source_'.$params->{'intra_alias_field'});
$resource->{other_cli} = $alias // $source_cli;
$other_normalize = 0;
# if there is an alias field (e.g. gpp0), use this
} elsif($item->source_account_id && $params->{'alias_field'}) {
my $alias = $item->get_column('source_'.$params->{'alias_field'});
$resource->{other_cli} = $alias // $source_cli;
$other_normalize = 1;
} else {
$resource->{other_cli} = $source_cli;
$other_normalize = 1;
}
$other_suppression = $source_cli_suppression;
}
$other_domain = $item->source_domain;
}
@ -221,13 +242,40 @@ sub process_cdr_item {
$resource->{other_cli} = $normalized_cli;
}
}
my @own_details = ();
if ($own_suppression) {
$c->log->debug('own suppression: id = ' . $own_suppression->id . ' direction = ' . $own_suppression->direction .
' mode = ' . $own_suppression->mode . ' domain = ' . $own_suppression->domain . ' pattern = ' . $own_suppression->pattern);
if (_is_show_suppressions($c)) {
push(@own_details,_localize_detail($c,'obfuscated',$own_suppression->label)) if 'obfuscate' eq $own_suppression->mode;
push(@own_details,_localize_detail($c,'filtered',$own_suppression->label)) if 'filter' eq $own_suppression->mode;
} else {
$resource->{own_cli} = $own_suppression->label;
}
}
if ( (!($sub // $own_sub)) || (($sub // $own_sub)->status eq "terminated") ) {
$resource->{own_cli} .= " (terminated)";
push(@own_details,_localize_detail($c,'terminated'));
#$resource->{own_cli} .= " (terminated)";
}
$resource->{own_cli} .= ' (' . join(', ',@own_details) . ')' if (scalar @own_details) > 0;
my @other_details = ();
if ($other_suppression) {
$c->log->debug('other suppression: id = ' . $other_suppression->id . ' direction = ' . $other_suppression->direction .
' mode = ' . $other_suppression->mode . ' domain = ' . $other_suppression->domain . ' pattern = ' . $other_suppression->pattern);
if (_is_show_suppressions($c)) {
push(@other_details,_localize_detail($c,'obfuscated',$other_suppression->label)) if 'obfuscate' eq $other_suppression->mode;
push(@other_details,_localize_detail($c,'filtered',$other_suppression->label)) if 'filter' eq $other_suppression->mode;
} else {
$resource->{other_cli} = $other_suppression->label;
}
}
if ($other_sub && $other_sub->status eq "terminated" &&
$own_sub && $own_sub->contract_id == $other_sub->contract_id) {
$resource->{other_cli} .= " (terminated)";
push(@other_details,_localize_detail($c,'terminated'));
#$resource->{other_cli} .= " (terminated)";
}
$resource->{other_cli} .= ' (' . join(', ',@other_details) . ')' if (scalar @other_details) > 0;
$resource->{status} = $item->call_status;
$resource->{rating_status} = $item->rating_status;
@ -245,9 +293,287 @@ sub process_cdr_item {
$item->source_customer_free_time : 0;
return $resource;
}
sub _get_suppressions {
my ($c,$item) = @_;
my ($source_cli_suppression,$destination_user_in_suppression);
if (ENABLE_SUPPRESSIONS) {
my $supressions_rs = $c->model('DB')->resultset('call_list_suppressions');
$source_cli_suppression = $supressions_rs->find($item->get_column(SOURCE_CLI_SUPPRESSION_ID_COLNAME))
if defined $item->get_column(SOURCE_CLI_SUPPRESSION_ID_COLNAME);
$destination_user_in_suppression = $supressions_rs->find($item->get_column(DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME))
if defined $item->get_column(DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME);
}
return ($source_cli_suppression,$destination_user_in_suppression);
}
sub _localize_detail {
my ($c,@params) = @_;
if (Scalar::Util::blessed($c) and $c->can('loc')) { #prepare for use with $c stub in generate_invoices.pl ...
@params = map { $c->loc($_); } @params;
}
if ((scalar @params) == 2) {
return sprintf('%s: %s',$params[0],$params[1]);
} elsif ((scalar @params) == 1) {
return sprintf('%s',$params[0]);
}
return '';
}
sub _is_show_suppressions {
my $c = shift;
return ($c->user->roles eq "admin" or $c->user->roles eq "reseller");
}
sub call_list_suppressions_rs {
my ($c,$rs,$mode) = @_;
return $rs unless ENABLE_SUPPRESSIONS;
my %search_cond = ();
my %search_xtra = ();
if (_is_show_suppressions($c)) {
if (defined $mode and SUPPRESS_OUT == $mode) {
$search_xtra{'+select'} = [
#{ '' => \[ 'me.source_cli' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ _get_call_list_suppression_sq('outgoing',qw(filter obfuscate)) ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
} elsif (defined $mode and SUPPRESS_IN == $mode) {
$search_xtra{'+select'} = [
{ '' => \[ _get_call_list_suppression_sq('incoming',qw(filter obfuscate)) ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
#{ '' => \[ 'me.destination_user_in' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
} elsif (defined $mode and SUPPRESS_INOUT == $mode) {
$search_xtra{'+select'} = [
{ '' => \[ _get_call_list_suppression_sq('incoming',qw(filter obfuscate)) ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ _get_call_list_suppression_sq('outgoing',qw(filter obfuscate)) ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
} else {
$search_xtra{'+select'} = [
#{ '' => \[ 'me.source_cli' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
#{ '' => \[ 'me.destination_user_in' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
}
} else {
if (defined $mode and SUPPRESS_OUT == $mode) {
$search_xtra{'+select'} = [
#{ '' => \[ 'me.source_cli' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ _get_call_list_suppression_sq('outgoing',qw(obfuscate)) ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
$search_cond{'-not exists'} = \[ '('._get_call_list_suppression_sq('outgoing',qw(filter)).')' ];
} elsif (defined $mode and SUPPRESS_IN == $mode) {
$search_xtra{'+select'} = [
{ '' => \[ _get_call_list_suppression_sq('incoming',qw(obfuscate)) ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
#{ '' => \[ 'me.destination_user_in' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
$search_cond{'-not exists'} = \[ '('._get_call_list_suppression_sq('incoming',qw(filter)).')' ];
} elsif (defined $mode and SUPPRESS_INOUT == $mode) {
$search_xtra{'+select'} = [
{ '' => \[ _get_call_list_suppression_sq('incoming',qw(obfuscate)) ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ _get_call_list_suppression_sq('outgoing',qw(obfuscate)) ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
$search_cond{'-and'} = [
{ '-not exists' => \[ '('._get_call_list_suppression_sq('incoming',qw(filter)).')' ] },
{ '-not exists' => \[ '('._get_call_list_suppression_sq('outgoing',qw(filter)).')' ] },
];
} else {
$search_xtra{'+select'} = [
#{ '' => \[ 'me.source_cli' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => SOURCE_CLI_SUPPRESSION_ID_COLNAME },
#{ '' => \[ 'me.destination_user_in' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
{ '' => \[ 'NULL' ] , -as => DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME },
];
}
}
return $rs->search_rs(\%search_cond,\%search_xtra);
}
sub _get_call_list_suppression_sq {
my ($direction,@modes) = @_;
my $domain_col;
my $number_col;
if ('incoming' eq $direction) {
$domain_col = 'destination_domain';
$number_col = 'source_cli';
} else {
$domain_col = 'source_domain';
$number_col = 'destination_user_in';
}
return "select id from billing.call_list_suppressions where direction = \"$direction\" and mode in (".join(',',map { '"'.$_.'"'; } @modes).")".
" and (domain = \"\" or domain = me.$domain_col) and me.$number_col regexp pattern limit 1";
}
sub get_suppression_id_colnames {
my @cols = ();
return @cols unless ENABLE_SUPPRESSIONS;
push(@cols,SOURCE_CLI_SUPPRESSION_ID_COLNAME);
push(@cols,DESTINATION_USER_IN_SUPPRESSION_ID_COLNAME);
return @cols;
}
sub suppress_cdr_fields {
my ($c,$resource,$item) = @_;
#use Data::Dumper;
#$c->log->debug(Dumper($resource));
return $resource unless ENABLE_SUPPRESSIONS;
my ($source_cli_suppression,$destination_user_in_suppression) = _get_suppressions($c,$item);
if (exists $resource->{source_cli} and defined $source_cli_suppression) {
$c->log->debug('source_cli suppression: id = ' . $source_cli_suppression->id . ' direction = ' . $source_cli_suppression->direction .
' mode = ' . $source_cli_suppression->mode . ' domain = ' . $source_cli_suppression->domain . ' pattern = ' . $source_cli_suppression->pattern);
my @source_cli_details = ();
if (_is_show_suppressions($c)) {
push(@source_cli_details,_localize_detail($c,'obfuscated',$source_cli_suppression->label)) if 'obfuscate' eq $source_cli_suppression->mode;
push(@source_cli_details,_localize_detail($c,'filtered',$source_cli_suppression->label)) if 'filter' eq $source_cli_suppression->mode;
} else {
$resource->{source_cli} = $source_cli_suppression->label;
}
$resource->{source_cli} .= ' (' . join(', ',@source_cli_details) . ')' if (scalar @source_cli_details) > 0;
}
if (exists $resource->{destination_user_in} and defined $destination_user_in_suppression) {
$c->log->debug('destination_user_in suppression: id = ' . $destination_user_in_suppression->id . ' direction = ' . $destination_user_in_suppression->direction .
' mode = ' . $destination_user_in_suppression->mode . ' domain = ' . $destination_user_in_suppression->domain . ' pattern = ' . $destination_user_in_suppression->pattern);
my @destination_user_in_details = ();
if (_is_show_suppressions($c)) {
push(@destination_user_in_details,_localize_detail($c,'obfuscated',$destination_user_in_suppression->label)) if 'obfuscate' eq $destination_user_in_suppression->mode;
push(@destination_user_in_details,_localize_detail($c,'filtered',$destination_user_in_suppression->label)) if 'filter' eq $destination_user_in_suppression->mode;
} else {
$resource->{destination_user_in} = $destination_user_in_suppression->label;
}
$resource->{destination_user_in} .= ' (' . join(', ',@destination_user_in_details) . ')' if (scalar @destination_user_in_details) > 0;
}
return $resource;
}
#sub _get_call_list_suppressions_rs {
# my ($c,$direction,@modes) = @_;
# my $domain_col;
# my $number_col;
# if ('incoming' eq $direction) {
# $domain_col = 'destination_domain';
# $number_col = 'source_cli';
# } else {
# $domain_col = 'source_domain';
# $number_col = 'destination_user_in';
# }
# return $c->model('DB')->resultset('call_list_suppressions')->search_rs({
# direction => { '=' => $direction },
# mode => { 'in' => \@modes },
# '-or' => [{
# domain => '',
# },{
# domain => \"me.$domain_col",
# }],
# "me.$number_col" => { 'regexp' => \'pattern' },
# });
#
#}
sub _insert_suppressions_csv_batch {
my ($c, $schema, $records, $chunk_size) = @_;
NGCP::Panel::Utils::MySQL::bulk_insert(
c => $c,
schema => $schema,
do_transaction => 0,
query => "INSERT INTO billing.call_list_suppressions(domain,direction,pattern,mode,label)",
data => $records,
chunk_size => $chunk_size
);
}
sub upload_suppressions_csv {
my(%params) = @_;
my ($c,$data,$schema) = @params{qw/c data schema/};
my ($start, $end);
# csv bulk upload
my $csv = Text::CSV_XS->new({ allow_whitespace => 1, binary => 1, keep_meta_info => 1 });
#my @cols = @{ $c->config->{lnp_csv}->{element_order} };
my @cols = qw/domain direction pattern mode label/;
my @fields ;
my @fails = ();
my $linenum = 0;
my @suppressions = ();
open(my $fh, '<:encoding(utf8)', $data);
$start = time;
my $chunk_size = 2000;
while ( my $line = $csv->getline($fh)) {
++$linenum;
unless (scalar @{ $line } == scalar @cols) {
push @fails, $linenum;
next;
}
my $row = {};
@{$row}{@cols} = @{ $line };
push @suppressions, [ $row->{domain}, $row->{direction}, $row->{pattern}, $row->{mode}, $row->{label} ];
if($linenum % $chunk_size == 0) {
_insert_suppressions_csv_batch($c, $schema, \@suppressions, $chunk_size);
@suppressions = ();
}
}
if(@suppressions) {
_insert_suppressions_csv_batch($c, $schema, \@suppressions, $chunk_size);
}
$end = time;
close $fh;
$c->log->debug("Parsing and uploading call list suppression CSV took " . ($end - $start) . "s");
my $text = $c->loc('Call list suppressions successfully uploaded');
if(@fails) {
$text .= $c->loc(", but skipped the following line numbers: ") . (join ", ", @fails);
}
return ( \@fails, \$text );
}
sub create_suppressions_csv {
my(%params) = @_;
my($c, $rs) = @params{qw/c rs/};
$rs //= $c->stash->{rs} // $c->model('DB')->resultset('call_list_suppressions');
#my @cols = @{ $c->config->{lnp_csv}->{element_order} };
my @cols = qw/domain direction pattern mode label/;
my ($start, $end);
$start = time;
while(my $row = $rs->next) {
my %cuppression = $row->get_inflated_columns;
#delete $lnp{id};
$c->res->write_fh->write(join (",", @cuppression{@cols}) );
$c->res->write_fh->write("\n");
}
$c->res->write_fh->close;
$end = time;
$c->log->debug("Creating call list suppression CSV for download took " . ($end - $start) . "s");
return 1;
}
1;

@ -6,6 +6,7 @@ use Sipwise::Base;
use DBIx::Class::Exception;
use NGCP::Panel::Utils::DateTime;
use DateTime::Format::Strptime qw();
use NGCP::Panel::Utils::CallList qw();
sub recursively_lock_contract {
my %params = @_;
@ -296,7 +297,7 @@ sub get_contract_calls_rs{
$stime ||= NGCP::Panel::Utils::DateTime::current_local()->truncate( to => 'month' );
$etime ||= $stime->clone->add( months => 1 );
my $calls_rs = $c->model('DB')->resultset('cdr')->search( {
my $calls_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search_rs( {
# source_user_id => { 'in' => [ map {$_->uuid} @{$contract->{subscriber}} ] },
'call_status' => 'ok',
'source_user_id' => { '!=' => '0' },
@ -306,28 +307,23 @@ sub get_contract_calls_rs{
{ '<=' => $etime->epoch},
],
'source_account_id' => $customer_contract_id,
},{
select => [qw/
source_user source_domain source_cli
destination_user_in
start_time duration call_type
source_customer_cost
source_customer_billing_zones_history.zone
source_customer_billing_zones_history.detail
/],
as => [qw/
source_user source_domain source_cli
destination_user_in
start_time duration call_type
source_customer_cost
zone
zone_detail
/],
},undef ),NGCP::Panel::Utils::CallList::SUPPRESS_INOUT);
my @cols = ();
push(@cols,qw/source_user source_domain source_cli destination_user_in/);
push(@cols,NGCP::Panel::Utils::CallList::get_suppression_id_colnames());
push(@cols,qw/start_time duration call_type source_customer_cost/);
my @colnames = @cols;
push(@cols,qw/source_customer_billing_zones_history.zone source_customer_billing_zones_history.detail/);
push(@colnames,qw/zone zone_detail/);
return $calls_rs->search_rs(undef,{
select => \@cols,
as => \@colnames,
'join' => 'source_customer_billing_zones_history',
'order_by' => 'start_time',
} );
return $calls_rs;
}
sub prepare_billing_mappings {

@ -6,6 +6,7 @@ use strict;
use NGCP::Panel::Utils::DateTime;
use DateTime::Format::Strptime;
use URI::Escape;
use NGCP::Panel::Utils::CallList qw();
sub template {
return 'widgets/subscriber_calls_overview.tt';
@ -23,12 +24,12 @@ sub filter {
sub _prepare_calls {
my ($self, $c) = @_;
my $out_rs = $c->model('DB')->resultset('cdr')->search({
my $out_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search_rs({
source_user_id => $c->user->uuid,
});
my $in_rs = $c->model('DB')->resultset('cdr')->search({
}),NGCP::Panel::Utils::CallList::SUPPRESS_OUT);
my $in_rs = NGCP::Panel::Utils::CallList::call_list_suppressions_rs($c,$c->model('DB')->resultset('cdr')->search_rs({
destination_user_id => $c->user->uuid,
});
}),NGCP::Panel::Utils::CallList::SUPPRESS_IN);
my $calls_rs = $out_rs->union_all($in_rs);
$c->stash(calls_rs => $calls_rs);
@ -70,7 +71,7 @@ sub calls_slice {
$resource{source_user_id} = $call->{source_user_id};
$resource{start_time} = $datetime_fmt->format_datetime($call->{start_time});
$resource{duration} = NGCP::Panel::Utils::DateTime::sec_to_hms($c,$call->{duration});
\%resource;
NGCP::Panel::Utils::CallList::suppress_cdr_fields($c,\%resource,$_);
} $c->stash->{calls_rs}->search(undef, {
order_by => { -desc => 'me.start_time' },
})->slice(0, 4)->all ];

@ -0,0 +1,38 @@
[% site_config.title = c.loc('Global Call List Suppressions') -%]
<div class="row">
<span>
<a class="btn btn-primary btn-large" href="[% c.uri_for('/back') %]"><i class="icon-arrow-left"></i> [% c.loc('Back') %]</a>
<a class="btn btn-primary btn-large" href="[% c.uri_for('/calllistsuppression/download') %]"><i class="icon-star"></i> [% c.loc('Download CSV') %]</a>
<a class="btn btn-primary btn-large" href="[% c.uri_for('/calllistsuppression/upload') %]"><i class="icon-star"></i> [% c.loc('Upload CSV') %]</a>
</span>
</div>
[% back_created = 1 -%]
[%
helper.name = c.loc('Call List Suppression');
helper.identifier = "call_list_suppression";
helper.messages = messages;
helper.dt_columns = calllistsuppression_dt_columns;
helper.paginate = 'true';
helper.filter = 'true';
helper.close_target = close_target;
helper.create_flag = create_flag;
helper.edit_flag = edit_flag;
helper.form_object = form;
helper.length_change = 1;
helper.ajax_uri = c.uri_for_action('/calllistsuppression/ajax');
UNLESS c.user.read_only;
helper.dt_buttons = [
{ name = c.loc('Delete'), uri = "/calllistsuppression/'+full[\"id\"]+'/delete", class = 'btn-small btn-secondary', icon = 'icon-remove' },
{ name = c.loc('Edit'), uri = "/calllistsuppression/'+full[\"id\"]+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' },
];
helper.top_buttons = [
{ name = c.loc('Create call list suppression'), uri = c.uri_for_action('/calllistsuppression/create'), icon = 'icon-star' },
];
END;
PROCESS 'helpers/datatables.tt';
-%]

@ -56,9 +56,10 @@
<li><a href="[% c.uri_for('/domain') %]">[% c.loc('Domains') %]</a></li>
<li><a href="[% c.uri_for('/subscriber') %]">[% c.loc('Subscribers') %]</a></li>
<li><a href="[% c.uri_for('/subscriberprofile') %]">[% c.loc('Subscriber Profiles') %]</a></li>
<li><a href="[% c.uri_for('/calllistsuppression') %]">[% c.loc('Call List Suppressions') %]</a></li>
<li><a href="[% c.uri_for('/billing') %]">[% c.loc('Billing') %]</a></li>
<li><a href="[% c.uri_for('/network') %]">[% c.loc('Billing Networks') %]</a></li>
<li><a href="[% c.uri_for('/package') %]">[% c.loc('Profile Packages') %]</a></li>
<li><a href="[% c.uri_for('/package') %]">[% c.loc('Profile Packages') %]</a></li>
<li><a href="[% c.uri_for('/invoicetemplate') %]">[% c.loc('Invoice Templates') %]</a></li>
<li><a href="[% c.uri_for('/invoice') %]">[% c.loc('Invoices') %]</a></li>
[% IF c.config.features.voucher -%]

Loading…
Cancel
Save