diff --git a/lib/NGCP/Panel.pm b/lib/NGCP/Panel.pm index 2f82371449..cca74dfee4 100644 --- a/lib/NGCP/Panel.pm +++ b/lib/NGCP/Panel.pm @@ -91,7 +91,7 @@ __PACKAGE__->config( 'View::JSON' => { #Set the stash keys to be exposed to a JSON response #(sEcho iTotalRecords iTotalDisplayRecords aaData) for datatables - expose_stash => [ qw(sEcho iTotalRecords iTotalDisplayRecords aaData dt_custom_footer widget_data timeline_data) ], + expose_stash => [ qw(sEcho iTotalRecords iTotalDisplayRecords iTotalRecordCountClipped iTotalDisplayRecordCountClipped aaData dt_custom_footer widget_data timeline_data) ], }, 'View::TT' => { INCLUDE_PATH => [ @@ -266,13 +266,13 @@ __PACKAGE__->log(Log::Log4perl::Catalyst->new($logger_config)); my ($self, $root, $property_type, $relation, $property) = @_; my $embedded = $root->embedded ? $root->embedded->[0] : undef; if ($embedded - && ( ( $property_type eq 'links' && $panel_config->{appearance}{api_links_forcearray} ) + && ( ( $property_type eq 'links' && $panel_config->{appearance}{api_links_forcearray} ) || ( $property_type eq 'embedded' && $panel_config->{appearance}{api_embedded_forcearray}) ) && $relation =~/^ngcp:[a-z0-9]+$/ ) { return 1; } - if (!$embedded + if (!$embedded && ( ( $property_type eq 'links' && $panel_config->{appearance}{api_links_forcearray} ) ) && $relation =~/^ngcp:[a-z0-9]+$/ ) { diff --git a/lib/NGCP/Panel/Controller/Contact.pm b/lib/NGCP/Panel/Controller/Contact.pm index 4f637566c6..a2b3f1323c 100644 --- a/lib/NGCP/Panel/Controller/Contact.pm +++ b/lib/NGCP/Panel/Controller/Contact.pm @@ -372,6 +372,8 @@ sub countries_ajax :Chained('/') :PathPart('contact/country/ajax') :Args(0) :Doe $c->stash(aaData => \@aaData, iTotalRecords => $count, iTotalDisplayRecords => $count, + iTotalRecordCountClipped => \0, + iTotalDisplayRecordCountClipped => \0, sEcho => int($c->request->params->{sEcho} // 1), ); diff --git a/lib/NGCP/Panel/Controller/Device.pm b/lib/NGCP/Panel/Controller/Device.pm index db1dbdc93d..e18760678b 100644 --- a/lib/NGCP/Panel/Controller/Device.pm +++ b/lib/NGCP/Panel/Controller/Device.pm @@ -46,14 +46,14 @@ sub base :Chained('/') :PathPart('device') :CaptureArgs(0) { } ], }); - + $reseller_id and $devmod_rs = $devmod_rs->search({ reseller_id => $reseller_id }); $c->stash->{devmod_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => 'id', search => 1, title => $c->loc('#') }, { name => 'type', search => 1, title => $c->loc('Type') }, { name => 'reseller.name', search => 1, title => $c->loc('Reseller') }, { name => 'vendor', search => 1, title => $c->loc('Vendor') }, - { name => 'model', search => 1, title => $c->loc('Model') }, + { name => 'model', search => 1, title => $c->loc('Model') }, ]); my $devfw_rs = $c->model('DB')->resultset('autoprov_firmwares')->search_rs(undef,{'columns' => [qw/id device_id version filename tag/], @@ -154,7 +154,7 @@ sub devmod_ajax :Chained('base') :PathPart('model/ajax') :Args(0) :Does(ACL) :AC NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{devmod_dt_columns}, sub { my ($result) = @_; - my %data = ( + my %data = ( mac_image_exists => $result->get_column('mac_image_exists'), front_image_exists => $result->get_column('front_image_exists'), ); @@ -173,7 +173,7 @@ sub extensionmodel_ajax :Chained('base') :PathPart('extensionmodel/ajax') :Args( NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{devmod_dt_columns}, sub { my ($result) = @_; - my %data = ( + my %data = ( mac_image_exists => $result->get_column('mac_image_exists'), front_image_exists => $result->get_column('front_image_exists'), ); @@ -663,7 +663,7 @@ sub devfw_download :Chained('devfw_base') :PathPart('download') :Args(0) { $c->response->content_type('application/octet-stream'); $c->response->body( NGCP::Panel::Utils::DeviceFirmware::get_firmware_data( - c => $c, + c => $c, fw_id => $fw->id )); } @@ -952,6 +952,8 @@ sub devprof_extensions :Chained('devprof_base') :PathPart('extensions') :Args(0) aaData => $data, iTotalRecords => 1, iTotalDisplayRecords => 1, + iTotalRecordCountClipped => \0, + iTotalDisplayRecordCountClipped => \0, sEcho => int($c->request->params->{sEcho} // 1), ); @@ -1020,6 +1022,8 @@ sub get_annotated_info :Private { aaData => $data, iTotalRecords => 1, iTotalDisplayRecords => 1, + iTotalRecordCountClipped => \0, + iTotalDisplayRecordCountClipped => \0, sEcho => int($c->request->params->{sEcho} // 1), ); @@ -1194,7 +1198,7 @@ sub dev_field_config :Chained('/') :PathPart('device/autoprov/config') :Args() { $c->response->status(403); return; } - } + } my $dev = $c->model('DB')->resultset('autoprov_field_devices')->find({ identifier => $id @@ -1364,16 +1368,16 @@ sub dev_field_config :Chained('/') :PathPart('device/autoprov/config') :Args() { '+select' => [\'replace(attribute.attribute,"__","")' ], '+as' => ['attribute_normalized'], }); - $preferences_device_dynamic->{$type} = get_inflated_columns_all($pref_rs_dynamic, - 'hash' => 'attribute_normalized', - 'column' => 'value' + $preferences_device_dynamic->{$type} = get_inflated_columns_all($pref_rs_dynamic, + 'hash' => 'attribute_normalized', + 'column' => 'value' ); my $pref_rs_static = $pref_rs->search_rs({ 'attribute.dynamic' => 0, }); - $preferences_device->{$type} = get_inflated_columns_all($pref_rs_static, - 'hash' => 'attribute', - 'column' => 'value' + $preferences_device->{$type} = get_inflated_columns_all($pref_rs_static, + 'hash' => 'attribute', + 'column' => 'value' ); } my %preferences_device = ( @@ -1888,7 +1892,7 @@ sub dev_field_firmware_download :Chained('dev_field_firmware_base') :PathPart('v $c->response->content_type('application/octet-stream'); $c->response->body( NGCP::Panel::Utils::DeviceFirmware::get_firmware_data( - c => $c, + c => $c, fw_id => $fw->id )); } @@ -1939,7 +1943,7 @@ sub dev_field_firmware_next :Chained('dev_field_firmware_version_base') :PathPar $c->response->content_type('application/octet-stream'); $c->response->body( NGCP::Panel::Utils::DeviceFirmware::get_firmware_data( - c => $c, + c => $c, fw_id => $fw->id )); } @@ -1976,7 +1980,7 @@ sub dev_field_firmware_latest :Chained('dev_field_firmware_version_base') :PathP $c->response->content_type('application/octet-stream'); $c->response->body( NGCP::Panel::Utils::DeviceFirmware::get_firmware_data( - c => $c, + c => $c, fw_id => $fw->id )); } @@ -1995,7 +1999,7 @@ sub devices_preferences_list :Chained('devmod_base') :PathPart('preferences') :C NGCP::Panel::Utils::Preferences::load_preference_list( c => $c, pref_values => $pref_values, - #we don't need fielddev_pref flag, because it always will be just more narrow than dev_pref. + #we don't need fielddev_pref flag, because it always will be just more narrow than dev_pref. dev_pref => 1, search_conditions => [{ 'attribute' => @@ -2003,7 +2007,7 @@ sub devices_preferences_list :Chained('devmod_base') :PathPart('preferences') :C { 'like' => 'vnd_'.lc($c->stash->{devmod}->vendor).'%' }, {'-not_like' => 'vnd_%' }, ], - #relation type is defined by preference flag dev_pref, + #relation type is defined by preference flag dev_pref, #so here we select only linked to the current model, or not linked to any model at all '-or' => [ 'voip_preference_relations.autoprov_device_id' => $c->stash->{devmod}->id, @@ -2069,7 +2073,7 @@ sub devices_preferences_create :Chained('devices_preferences_list') :PathPart('c $resource->{autoprov_device_id} = $c->stash->{devmod}->id; my $preference = NGCP::Panel::Utils::Preferences::create_dynamic_preference( - $c, $resource, + $c, $resource, group_name => 'CPBX Device Administration', ); diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index 09ee9ed669..06b021ed67 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -386,6 +386,8 @@ sub emptyajax :Chained('/') :PathPart('emptyajax') :Args(0) { aaData => [], iTotalDisplayRecords => 0, iTotalRecords => 0, + iTotalRecordCountClipped => \0, + iTotalDisplayRecordCountClipped => \0, sEcho => $c->request->params->{sEcho} // 1, ); $c->detach( $c->view("JSON") ); diff --git a/lib/NGCP/Panel/Controller/Subscriber.pm b/lib/NGCP/Panel/Controller/Subscriber.pm index eda7013587..0708e559eb 100644 --- a/lib/NGCP/Panel/Controller/Subscriber.pm +++ b/lib/NGCP/Panel/Controller/Subscriber.pm @@ -3863,6 +3863,7 @@ sub _process_calls_rows { $data{total_customer_cost} = (defined $result->{total_customer_cost} ? sprintf("%.2f", $result->{total_customer_cost} / 100.0) : ""); return %data; }, + 'count_limit' => 1000, } ); } diff --git a/lib/NGCP/Panel/Utils/Datatables.pm b/lib/NGCP/Panel/Utils/Datatables.pm index 16b672cf6e..5fac8baeda 100644 --- a/lib/NGCP/Panel/Utils/Datatables.pm +++ b/lib/NGCP/Panel/Utils/Datatables.pm @@ -17,6 +17,8 @@ sub process { my $aaData = []; my $totalRecords = 0; my $displayRecords = 0; + my $totalRecordCountClipped = 0; + my $displayRecordCountClipped = 0; my $aggregate_cols = []; my $aggregations = {}; @@ -56,16 +58,16 @@ sub process { #all joins already implemented, and filters aren't applied. But count we will take only if there are search and no other aggregations my $totalRecords_rs = $rs; #= $use_rs_cb ? 0 : $rs->count; - + ### Search processing section - + # generic searching my @searchColumns = (); my %conjunctSearchColumns = (); #processing single search input - group1 from groups to be joined by 'AND' my $searchString = $c->request->params->{sSearch} // ""; if($searchString && ! $use_rs_cb) { - #for search string from one search input we need to check all columns which contain the 'search' spec (now: qw/search search_lower_column search_upper_column/). so, for example user entered into search input ip address - we don't know that it is ip address, so we check that name like search OR id like search OR search is between network_lower_value and network upper value + #for search string from one search input we need to check all columns which contain the 'search' spec (now: qw/search search_lower_column search_upper_column/). so, for example user entered into search input ip address - we don't know that it is ip address, so we check that name like search OR id like search OR search is between network_lower_value and network upper value foreach my $col(@{ $cols }) { my ($name,$search_value,$op,$convert); # avoid amigious column names if we have the same column in different joined tables @@ -171,21 +173,23 @@ sub process { $aggregations = {%{$aggregations}, $total_row_func->($aggregations) }; } - if(!$use_rs_cb){ - if(@searchColumns){ - $totalRecords = $totalRecords_rs->count; - if(!@$aggregate_cols){ - $displayRecords = $rs->count; + if (!$use_rs_cb) { + if (@searchColumns) { + ($totalRecords, $totalRecordCountClipped) = get_count_safe($c,$totalRecords_rs,$params); + if (!@$aggregate_cols) { + ($displayRecords, $displayRecordCountClipped) = get_count_safe($c,$rs,$params); } - }else{ - if(@$aggregate_cols){ + } else { + if (@$aggregate_cols) { $totalRecords = $displayRecords; - }elsif(!@$aggregate_cols){ - $totalRecords = $displayRecords = $totalRecords_rs->count; + } elsif (!@$aggregate_cols) { + ($totalRecords, $totalRecordCountClipped) = get_count_safe($c,$totalRecords_rs,$params); + $displayRecords = $totalRecords; + $displayRecordCountClipped = $totalRecordCountClipped; } } } - + # show specific row on top (e.g. if we come back from a newly created entry) my $topId = $c->request->params->{iIdOnTop}; if(defined $topId) { @@ -257,10 +261,37 @@ sub process { add_arbitrary_data($c, $aaData, $params->{topData}, $cols, $row_func, $params); - expose_data($c, $aaData, $totalRecords, $displayRecords); + expose_data($c, $aaData, $totalRecords, $totalRecordCountClipped, $displayRecords, $displayRecordCountClipped); } +sub get_count_safe { + my ($c,$rs,$params) = @_; + my $count_limit = $params->{count_limit}; + #$count_limit = 12; + if ($c and defined $count_limit and $count_limit > 0) { + my ($count_clipped) = $c->model('DB')->storage->dbh_do(sub { + my ($storage, $dbh, $stmt, @bind_vals) = @_; + @bind_vals = map { $_->[1]; } @bind_vals; + $c->log->debug("bind: " . join(",",@bind_vals)); + return $dbh->selectrow_array("select count(1) from ($stmt) as query_clipped",undef,@bind_vals); + },@{${$rs->search_rs(undef,{ + page => 1, + rows => $count_limit + 1, + #below is required if fields with identical name are selected by $rs: + 'select' => (defined $params->{count_projection_column} ? $params->{count_projection_column} : "id"), + })->as_query}}); + if ($count_clipped > $count_limit) { + $c->log->debug("result count clipped"); + return ($count_limit,1); + } else { + return ($count_clipped,0); + } + } else { + return ($rs->count,0); + } +} + sub add_arbitrary_data { my ($c, $aaData, $topData, $cols, $row_func, $params) = @_; # show any arbitrary data rows on top, just like a union would do @@ -283,11 +314,13 @@ sub add_arbitrary_data { } sub expose_data { - my($c, $aaData, $totalRecords, $displayRecords) = @_; + my($c, $aaData, $totalRecords, $totalRecordCountClipped, $displayRecords, $displayRecordCountClipped) = @_; $c->stash( aaData => $aaData, iTotalRecords => $totalRecords, iTotalDisplayRecords => $displayRecords, + iTotalRecordCountClipped => ($totalRecordCountClipped ? \1 : \0), + iTotalDisplayRecordCountClipped => ($displayRecordCountClipped ? \1 : \0), sEcho => int($c->request->params->{sEcho} // 1), ); } @@ -304,7 +337,7 @@ sub process_static_data { add_arbitrary_data($c, $aaData, $data, $cols, $row_func, $params); my $totalRecords = scalar @$aaData; my $displayRecords = $totalRecords; - expose_data($c, $aaData, $totalRecords, $displayRecords); + expose_data($c, $aaData, $totalRecords, 0, $displayRecords, 0); } sub set_columns { diff --git a/share/static/js/libs/jquery.dataTables.js b/share/static/js/libs/jquery.dataTables.js index 27a84d9c46..7ce0a2d1b1 100644 --- a/share/static/js/libs/jquery.dataTables.js +++ b/share/static/js/libs/jquery.dataTables.js @@ -80,7 +80,7 @@ */ var DataTable; - + /* * It is useful to have variables which are scoped locally so only the * DataTables functions can access them and they don't leak into global space. @@ -89,43 +89,43 @@ * by DataTables as private variables here. This also ensures that there is no * clashing of variable names and that they can easily referenced for reuse. */ - - + + // Defined else where // _selector_run // _selector_opts // _selector_first // _selector_row_indexes - + var _ext; // DataTable.ext var _Api; // DataTable.Api var _api_register; // DataTable.Api.register var _api_registerPlural; // DataTable.Api.registerPlural - + var _re_dic = {}; var _re_new_lines = /[\r\n]/g; var _re_html = /<.*?>/g; var _re_date_start = /^[\w\+\-]/; var _re_date_end = /[\w\+\-]$/; - + // Escape regular expression special characters var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); - + // U+2009 is thin space and U+202F is narrow no-break space, both used in many // standards as thousands separators var _re_formatted_numeric = /[',$£€¥%\u2009\u202F]/g; - - + + var _empty = function ( d ) { return !d || d === true || d === '-' ? true : false; }; - - + + var _intVal = function ( s ) { var integer = parseInt( s, 10 ); return !isNaN(integer) && isFinite(s) ? integer : null; }; - + // Convert from a formatted number with characters other than `.` as the // decimal place, to a Javascript number var _numToDecimal = function ( num, decimalPoint ) { @@ -137,34 +137,34 @@ num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) : num; }; - - + + var _isNumber = function ( d, decimalPoint, formatted ) { var strType = typeof d === 'string'; - + if ( decimalPoint && strType ) { d = _numToDecimal( d, decimalPoint ); } - + if ( formatted && strType ) { d = d.replace( _re_formatted_numeric, '' ); } - + return _empty( d ) || (!isNaN( parseFloat(d) ) && isFinite( d )); }; - - + + // A string without HTML in it can be considered to be HTML still var _isHtml = function ( d ) { return _empty( d ) || typeof d === 'string'; }; - - + + var _htmlNumeric = function ( d, decimalPoint, formatted ) { if ( _empty( d ) ) { return true; } - + var html = _isHtml( d ); return ! html ? null : @@ -172,12 +172,12 @@ true : null; }; - - + + var _pluck = function ( a, prop, prop2 ) { var out = []; var i=0, ien=a.length; - + // Could have the test in the loop for slightly smaller code, but speed // is essential here if ( prop2 !== undefined ) { @@ -194,18 +194,18 @@ } } } - + return out; }; - - + + // Basically the same as _pluck, but rather than looping over `a` we use `order` // as the indexes to pick from `a` var _pluck_order = function ( a, order, prop, prop2 ) { var out = []; var i=0, ien=order.length; - + // Could have the test in the loop for slightly smaller code, but speed // is essential here if ( prop2 !== undefined ) { @@ -220,16 +220,16 @@ out.push( a[ order[i] ][ prop ] ); } } - + return out; }; - - + + var _range = function ( len, start ) { var out = []; var end; - + if ( start === undefined ) { start = 0; end = len; @@ -238,34 +238,34 @@ end = start; start = len; } - + for ( var i=start ; i') .css( { @@ -518,22 +518,22 @@ ) ) .appendTo( 'body' ); - + var test = n.find('.test'); - + // IE6/7 will oversize a width 100% element inside a scrolling element, to // include the width of the scrollbar, while other browsers ensure the inner // element is contained without forcing scrolling browser.bScrollOversize = test[0].offsetWidth === 100; - + // In rtl text layout, some browsers (most, but not all) will place the // scrollbar on the left, rather than the right. browser.bScrollbarLeft = test.offset().left !== 1; - + n.remove(); } - - + + /** * Array.prototype reduce[Right] method, used for browsers which don't support * JS 1.6. Done this way to reduce code size, since we iterate either way @@ -546,28 +546,28 @@ i = start, value, isSet = false; - + if ( init !== undefined ) { value = init; isSet = true; } - + while ( i !== end ) { if ( ! that.hasOwnProperty(i) ) { continue; } - + value = isSet ? fn( value, that[i], i, that ) : that[i]; - + isSet = true; i += inc; } - + return value; } - + /** * Add a column to the list used for the table with default values * @param {object} oSettings dataTables settings object @@ -587,18 +587,18 @@ idx: iCol } ); oSettings.aoColumns.push( oCol ); - + // Add search object for column specific search. Note that the `searchCols[ iCol ]` // passed into extend can be undefined. This allows the user to give a default // with only some of the parameters defined, and also not give a default var searchCols = oSettings.aoPreSearchCols; searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] ); - + // Use the default column options function to initialise classes etc _fnColumnOptions( oSettings, iCol, $(nTh).data() ); } - - + + /** * Apply options for a column * @param {object} oSettings dataTables settings object @@ -611,50 +611,50 @@ var oCol = oSettings.aoColumns[ iCol ]; var oClasses = oSettings.oClasses; var th = $(oCol.nTh); - + // Try to get width information from the DOM. We can't get it from CSS // as we'd need to parse the CSS stylesheet. `width` option can override if ( ! oCol.sWidthOrig ) { // Width attribute oCol.sWidthOrig = th.attr('width') || null; - + // Style attribute var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/); if ( t ) { oCol.sWidthOrig = t[1]; } } - + /* User specified column options */ if ( oOptions !== undefined && oOptions !== null ) { // Backwards compatibility _fnCompatCols( oOptions ); - + // Map camel case parameters to their Hungarian counterparts _fnCamelToHungarian( DataTable.defaults.column, oOptions ); - + /* Backwards compatibility for mDataProp */ if ( oOptions.mDataProp !== undefined && !oOptions.mData ) { oOptions.mData = oOptions.mDataProp; } - + if ( oOptions.sType ) { oCol._sManualType = oOptions.sType; } - + // `class` is a reserved word in Javascript, so we need to provide // the ability to use a valid name for the camel case input if ( oOptions.className && ! oOptions.sClass ) { oOptions.sClass = oOptions.className; } - + $.extend( oCol, oOptions ); _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" ); - + /* iDataSort to be applied (backwards compatibility), but aDataSort will take * priority if defined */ @@ -664,22 +664,22 @@ } _fnMap( oCol, oOptions, "aDataSort" ); } - + /* Cache the data get and set functions for speed */ var mDataSrc = oCol.mData; var mData = _fnGetObjectDataFn( mDataSrc ); var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; - + var attrTest = function( src ) { return typeof src === 'string' && src.indexOf('@') !== -1; }; oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && ( attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter) ); - + oCol.fnGetData = function (rowData, type, meta) { var innerData = mData( rowData, type, undefined, meta ); - + return mRender && type ? mRender( innerData, type, rowData, meta ) : innerData; @@ -687,20 +687,20 @@ oCol.fnSetData = function ( rowData, val, meta ) { return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta ); }; - + // Indicate if DataTables should read DOM data as an object or array // Used in _fnGetRowElements if ( typeof mDataSrc !== 'number' ) { oSettings._rowReadObject = true; } - + /* Feature sorting overrides column specific when off */ if ( !oSettings.oFeatures.bSort ) { oCol.bSortable = false; th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called } - + /* Check that the class assignment is correct for sorting */ var bAsc = $.inArray('asc', oCol.asSorting) !== -1; var bDesc = $.inArray('desc', oCol.asSorting) !== -1; @@ -725,8 +725,8 @@ oCol.sSortingClassJUI = oClasses.sSortJUI; } } - - + + /** * Adjust the table column widths for new data. Note: you would probably want to * do a redraw after calling this function! @@ -739,24 +739,24 @@ if ( settings.oFeatures.bAutoWidth !== false ) { var columns = settings.aoColumns; - + _fnCalculateColumnWidths( settings ); for ( var i=0 , iLen=columns.length ; i