| Sync up recent work - warvox - VoIP based wardialing tool, forked from rapid7/w… | |
| Log | |
| Files | |
| Refs | |
| README | |
| --- | |
| commit 6fba0686fb93cf3157e42be1dff58a5b9cace5b0 | |
| parent 876a89b6d4cde9a8e6df003f1aa0c08565040afc | |
| Author: HD Moore <[email protected]> | |
| Date: Sun, 6 Jan 2013 21:34:24 -0600 | |
| Sync up recent work | |
| Diffstat: | |
| A app/assets/images/search.png | 0 | |
| M app/assets/javascripts/application… | 158 +++++++++++++++++++++++++++… | |
| A app/assets/javascripts/dataTables.… | 44 +++++++++++++++++++++++++++… | |
| A app/assets/javascripts/dataTables.… | 54 +++++++++++++++++++++++++++… | |
| A app/assets/javascripts/dataTables.… | 15 +++++++++++++++ | |
| A app/assets/javascripts/dataTables_… | 133 +++++++++++++++++++++++++++… | |
| A app/assets/javascripts/jobs/view_r… | 52 +++++++++++++++++++++++++++… | |
| A app/assets/javascripts/jquery.tabl… | 215 +++++++++++++++++++++++++++… | |
| D app/assets/stylesheets/application… | 12 ------------ | |
| A app/assets/stylesheets/application… | 74 +++++++++++++++++++++++++++… | |
| M app/assets/stylesheets/bootstrap_a… | 22 ++++++++++++++++++++++ | |
| M app/controllers/jobs_controller.rb | 115 ++++++++++++++++++++++++++++-… | |
| M app/helpers/application_helper.rb | 153 +++++++++++++++++++++++++++++… | |
| M app/models/job.rb | 13 +++++++++---- | |
| A app/views/jobs/_view_results.json.… | 20 ++++++++++++++++++++ | |
| M app/views/jobs/view_results.html.e… | 52 +++++++++++++++++----------… | |
| M app/views/layouts/application.html… | 22 ++++++++++++---------- | |
| M config/environments/development.rb | 6 ++++-- | |
| M config/routes.rb | 11 ++++++----- | |
| A db/migrate/20130106000000_add_inde… | 29 +++++++++++++++++++++++++++… | |
| M db/schema.rb | 19 ++++++++++++++++++- | |
| M lib/warvox/jobs/analysis.rb | 9 ++++++--- | |
| 22 files changed, 1159 insertions(+), 69 deletions(-) | |
| --- | |
| diff --git a/app/assets/images/search.png b/app/assets/images/search.png | |
| Binary files differ. | |
| diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/app… | |
| @@ -7,4 +7,162 @@ | |
| //= require bootstrap-lightbox | |
| //= require dataTables/jquery.dataTables | |
| //= require dataTables/jquery.dataTables.bootstrap | |
| +//= require dataTables.hiddenTitle | |
| +//= require dataTables.filteringDelay | |
| +//= require dataTables.fnReloadAjax | |
| +//= require jquery.table | |
| +//= require dataTables_overrides | |
| //= require highcharts | |
| + | |
| + | |
| + | |
| + | |
| +function getParameterByName(name) | |
| +{ | |
| + name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]"); | |
| + var regexS = "[\\?&]" + name + "=([^&#]*)"; | |
| + var regex = new RegExp(regexS); | |
| + var results = regex.exec(window.location.href); | |
| + if(results == null) | |
| + return ""; | |
| + else | |
| + return decodeURIComponent(results[1].replace(/\+/g, " ")); | |
| +} | |
| + | |
| + | |
| +/* | |
| + * If the given select element is set to "", disables every other element | |
| + * inside the select's form. | |
| + */ | |
| +function disable_fields_if_select_is_blank(select) { | |
| + var formElement = Element.up(select, "form"); | |
| + var fields = formElement.getElements(); | |
| + | |
| + Element.observe(select, "change", function(e) { | |
| + var v = select.getValue(); | |
| + for (var i in fields) { | |
| + if (fields[i] != select && fields[i].type && fields[i]… | |
| + if (v != "") { | |
| + fields[i].disabled = true | |
| + } else { | |
| + fields[i].disabled = false; | |
| + } | |
| + } | |
| + } | |
| + }); | |
| +} | |
| + | |
| +function enable_fields_with_checkbox(checkbox, div) { | |
| + var fields; | |
| + | |
| + if (!div) { | |
| + div = Element.up(checkbox, "fieldset") | |
| + } | |
| + | |
| + f = function(e) { | |
| + fields = div.descendants(); | |
| + var v = checkbox.getValue(); | |
| + for (var i in fields) { | |
| + if (fields[i] != checkbox && fields[i].type && fields[… | |
| + if (!v) { | |
| + fields[i].disabled = true | |
| + } else { | |
| + fields[i].disabled = false; | |
| + } | |
| + } | |
| + } | |
| + } | |
| + f(); | |
| + Element.observe(checkbox, "change", f); | |
| +} | |
| + | |
| +function placeholder_text(field, text) { | |
| + var formElement = Element.up(field, "form"); | |
| + var submitButton = Element.select(formElement, 'input[type="submit"]')… | |
| + | |
| + if (field.value == "") { | |
| + field.value = text; | |
| + field.setAttribute("class", "placeholder"); | |
| + } | |
| + | |
| + Element.observe(field, "focus", function(e) { | |
| + field.setAttribute("class", ""); | |
| + if (field.value == text) { | |
| + field.value = ""; | |
| + } | |
| + }); | |
| + Element.observe(field, "blur", function(e) { | |
| + if (field.value == "") { | |
| + field.setAttribute("class", "placeholder"); | |
| + field.value = text; | |
| + } | |
| + }); | |
| + submitButton.observe("click", function(e) { | |
| + if (field.value == text) { | |
| + field.value = ""; | |
| + } | |
| + }); | |
| +} | |
| + | |
| + | |
| +function submit_checkboxes_to(path, token) { | |
| + var f = document.createElement('form'); | |
| + f.style.display = 'none'; | |
| + | |
| + /* Set the post destination */ | |
| + f.method = "POST"; | |
| + f.action = path; | |
| + | |
| + /* Create the authenticity_token */ | |
| + var s = document.createElement('input'); | |
| + s.setAttribute('type', 'hidden'); | |
| + s.setAttribute('name', 'authenticity_token'); | |
| + s.setAttribute('value', token); | |
| + f.appendChild(s); | |
| + | |
| + /* Copy the checkboxes from the host form */ | |
| + $("input[type=checkbox]").each(function(i,e) { | |
| + if (e.checked) { | |
| + var c = document.createElement('input'); | |
| + c.setAttribute('type', 'hidden'); | |
| + c.setAttribute('name', e.getAttribute('name') ); | |
| + c.setAttribute('value', e.getAttribute('value') ); | |
| + f.appendChild(c); | |
| + } | |
| + }) | |
| + | |
| + /* Look for hidden variables in checkbox form */ | |
| + $("input[type=hidden]").each(function(i,e) { | |
| + if ( e.getAttribute('name').indexOf("[]") != -1 ) { | |
| + var c = document.createElement('input'); | |
| + c.setAttribute('type', 'hidden'); | |
| + c.setAttribute('name', e.getAttribute('name') ); | |
| + c.setAttribute('value', e.getAttribute('value') ); | |
| + f.appendChild(c); | |
| + } | |
| + }) | |
| + | |
| + /* Copy the search field from the host form */ | |
| + $("input#search").each(function (i,e) { | |
| + if (e.getAttribute("class") != "placeholder") { | |
| + var c = document.createElement('input'); | |
| + c.setAttribute('type', 'hidden'); | |
| + c.setAttribute('name', e.getAttribute('name') ); | |
| + c.setAttribute('value', e.value ); | |
| + f.appendChild(c); | |
| + } | |
| + }); | |
| + | |
| + /* Append to the main form body */ | |
| + document.body.appendChild(f); | |
| + f.submit(); | |
| + return false; | |
| +} | |
| + | |
| + | |
| +// Look for the other half of this in app/coffeescripts/forms.coffee | |
| +function enableSubmitButtons() { | |
| + $("form.formtastic input[type='submit']").each(function(elmt) { | |
| + elmt.removeClassName('disabled'); elmt.removeClassName('submitting'); | |
| + }); | |
| +} | |
| diff --git a/app/assets/javascripts/dataTables.filteringDelay.js b/app/assets/j… | |
| @@ -0,0 +1,44 @@ | |
| +jQuery.fn.dataTableExt.oApi.fnSetFilteringDelay = function ( oSettings, iDelay… | |
| + /* | |
| + * Inputs: object:oSettings - dataTables settings object - automa… | |
| + * integer:iDelay - delay in milliseconds | |
| + * Usage: $('#example').dataTable().fnSetFilteringDelay(250); | |
| + * Author: Zygimantas Berziunas (www.zygimantas.com) and Allan Ja… | |
| + * License: GPL v2 or BSD 3 point style | |
| + * Contact: zygimantas.berziunas /AT\ hotmail.com | |
| + */ | |
| + var | |
| + _that = this, | |
| + iDelay = (typeof iDelay == 'undefined') ? 250 : iDelay; | |
| + | |
| + this.each( function ( i ) { | |
| + jQuery.fn.dataTableExt.iApiIndex = i; | |
| + var | |
| + $this = this, | |
| + oTimerId = null, | |
| + sPreviousSearch = null, | |
| + anControl = jQuery( 'input', _that.fnSettings().aanFea… | |
| + | |
| + anControl.unbind( 'keyup' ).bind( 'keyup', function() { | |
| + var $$this = $this; | |
| + | |
| + if (sPreviousSearch === null || sPreviousSearch != anC… | |
| + window.clearTimeout(oTimerId); | |
| + sPreviousSearch = anControl.val(); | |
| + oTimerId = window.setTimeout(function() { | |
| + jQuery.fn.dataTableExt.iApiIndex = i; | |
| + _that.fnFilter( anControl.val() ); | |
| + }, iDelay); | |
| + } | |
| + }); | |
| + | |
| + return this; | |
| + } ); | |
| + return this; | |
| +} | |
| + | |
| +/* Example call | |
| +$(document).ready(function() { | |
| + $('.dataTable').dataTable().fnSetFilteringDelay(); | |
| +} ); */ | |
| + | |
| diff --git a/app/assets/javascripts/dataTables.fnReloadAjax.js b/app/assets/jav… | |
| @@ -0,0 +1,53 @@ | |
| +jQuery.fn.dataTableExt.oApi.fnReloadAjax = function ( oSettings, sNewSource, f… | |
| +{ | |
| + if ( typeof sNewSource != 'undefined' && sNewSource != null ) { | |
| + oSettings.sAjaxSource = sNewSource; | |
| + } | |
| + | |
| + // Server-side processing should just call fnDraw | |
| + if ( oSettings.oFeatures.bServerSide ) { | |
| + this.fnDraw(); | |
| + return; | |
| + } | |
| + | |
| + this.oApi._fnProcessingDisplay( oSettings, true ); | |
| + var that = this; | |
| + var iStart = oSettings._iDisplayStart; | |
| + var aData = []; | |
| + | |
| + this.oApi._fnServerParams( oSettings, aData ); | |
| + | |
| + oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aDa… | |
| + /* Clear the old information from the table */ | |
| + that.oApi._fnClearTable( oSettings ); | |
| + | |
| + /* Got the data - add it to the table */ | |
| + var aData = (oSettings.sAjaxDataProp !== "") ? | |
| + that.oApi._fnGetObjectDataFn( oSettings.sAjaxDataProp )( json ) : json; | |
| + | |
| + for ( var i=0 ; i<aData.length ; i++ ) | |
| + { | |
| + that.oApi._fnAddData( oSettings, aData[i] ); | |
| + } | |
| + | |
| + oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); | |
| + | |
| + if ( typeof bStandingRedraw != 'undefined' && bStandingRedraw === true ) | |
| + { | |
| + oSettings._iDisplayStart = iStart; | |
| + that.fnDraw( false ); | |
| + } | |
| + else | |
| + { | |
| + that.fnDraw(); | |
| + } | |
| + | |
| + that.oApi._fnProcessingDisplay( oSettings, false ); | |
| + | |
| + /* Callback user function - for event handlers etc */ | |
| + if ( typeof fnCallback == 'function' && fnCallback != null ) | |
| + { | |
| + fnCallback( oSettings ); | |
| + } | |
| + }, oSettings ); | |
| +}; | |
| +\ No newline at end of file | |
| diff --git a/app/assets/javascripts/dataTables.hiddenTitle.js b/app/assets/java… | |
| @@ -0,0 +1,15 @@ | |
| +jQuery.fn.dataTableExt.oSort['title-numeric-asc'] = function(a,b) { | |
| + var x = a.match(/title="*(-?[0-9]+)/)[1]; | |
| + var y = b.match(/title="*(-?[0-9]+)/)[1]; | |
| + x = parseFloat( x ); | |
| + y = parseFloat( y ); | |
| + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); | |
| +}; | |
| + | |
| +jQuery.fn.dataTableExt.oSort['title-numeric-desc'] = function(a,b) { | |
| + var x = a.match(/title="*(-?[0-9]+)/)[1]; | |
| + var y = b.match(/title="*(-?[0-9]+)/)[1]; | |
| + x = parseFloat( x ); | |
| + y = parseFloat( y ); | |
| + return ((x < y) ? 1 : ((x > y) ? -1 : 0)); | |
| +}; | |
| diff --git a/app/assets/javascripts/dataTables_overrides.js b/app/assets/javasc… | |
| @@ -0,0 +1,133 @@ | |
| +$.extend( $.fn.dataTableExt.oStdClasses, { | |
| + "sWrapper": "dataTables_wrapper form-inline" | |
| +} ); | |
| + | |
| + | |
| +/* API method to get paging information */ | |
| +$.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings ) | |
| +{ | |
| + return { | |
| + "iStart": oSettings._iDisplayStart, | |
| + "iEnd": oSettings.fnDisplayEnd(), | |
| + "iLength": oSettings._iDisplayLength, | |
| + "iTotal": oSettings.fnRecordsTotal(), | |
| + "iFilteredTotal": oSettings.fnRecordsDisplay(), | |
| + "iPage": Math.ceil( oSettings._iDisplayStart / oSetti… | |
| + "iTotalPages": Math.ceil( oSettings.fnRecordsDisplay() / oS… | |
| + }; | |
| +}; | |
| + | |
| +/* Bootstrap style pagination control */ | |
| +$.extend( $.fn.dataTableExt.oPagination, { | |
| + "bootstrap": { | |
| + "fnInit": function( oSettings, nPaging, fnDraw ) { | |
| + var oLang = oSettings.oLanguage.oPaginate; | |
| + var fnClickHandler = function ( e ) { | |
| + e.preventDefault(); | |
| + if ( oSettings.oApi._fnPageChange(oSettings, e… | |
| + fnDraw( oSettings ); | |
| + } | |
| + }; | |
| + | |
| + $(nPaging).addClass('pagination').append( | |
| + '<ul>'+ | |
| + '<li class="prev disabled"><a href="#"… | |
| + '<li class="next disabled"><a href="#"… | |
| + '</ul>' | |
| + ); | |
| + var els = $('a', nPaging); | |
| + $(els[0]).bind( 'click.DT', { action: "previous" }, fn… | |
| + $(els[1]).bind( 'click.DT', { action: "next" }, fnClic… | |
| + }, | |
| + | |
| + "fnUpdate": function ( oSettings, fnDraw ) { | |
| + var iListLength = 5; | |
| + var oPaging = oSettings.oInstance.fnPagingInfo(); | |
| + var an = oSettings.aanFeatures.p; | |
| + var i, j, sClass, iStart, iEnd, iHalf=Math.floor(iList… | |
| + | |
| + if ( oPaging.iTotalPages < iListLength) { | |
| + iStart = 1; | |
| + iEnd = oPaging.iTotalPages; | |
| + } | |
| + else if ( oPaging.iPage <= iHalf ) { | |
| + iStart = 1; | |
| + iEnd = iListLength; | |
| + } else if ( oPaging.iPage >= (oPaging.iTotalPages-iHal… | |
| + iStart = oPaging.iTotalPages - iListLength + 1; | |
| + iEnd = oPaging.iTotalPages; | |
| + } else { | |
| + iStart = oPaging.iPage - iHalf + 1; | |
| + iEnd = iStart + iListLength - 1; | |
| + } | |
| + | |
| + for ( i=0, iLen=an.length ; i<iLen ; i++ ) { | |
| + // Remove the middle elements | |
| + $('li:gt(0)', an[i]).filter(':not(:last)').rem… | |
| + | |
| + // Add the new list items and their event hand… | |
| + for ( j=iStart ; j<=iEnd ; j++ ) { | |
| + sClass = (j==oPaging.iPage+1) ? 'class… | |
| + $('<li '+sClass+'><a href="#">'+j+'</a… | |
| + .insertBefore( $('li:last', an… | |
| + .bind('click', function (e) { | |
| + e.preventDefault(); | |
| + oSettings._iDisplaySta… | |
| + fnDraw( oSettings ); | |
| + } ); | |
| + } | |
| + | |
| + // Add / remove disabled classes from the stat… | |
| + if ( oPaging.iPage === 0 ) { | |
| + $('li:first', an[i]).addClass('disable… | |
| + } else { | |
| + $('li:first', an[i]).removeClass('disa… | |
| + } | |
| + | |
| + if ( oPaging.iPage === oPaging.iTotalPages-1 |… | |
| + $('li:last', an[i]).addClass('disabled… | |
| + } else { | |
| + $('li:last', an[i]).removeClass('disab… | |
| + } | |
| + } | |
| + } | |
| + } | |
| +} ); | |
| + | |
| + | |
| +/* | |
| + * TableTools Bootstrap compatibility | |
| + * Required TableTools 2.1+ | |
| + */ | |
| +if ( $.fn.DataTable.TableTools ) { | |
| + // Set the classes that TableTools uses to something suitable for Boot… | |
| + $.extend( true, $.fn.DataTable.TableTools.classes, { | |
| + "container": "DTTT btn-group", | |
| + "buttons": { | |
| + "normal": "btn", | |
| + "disabled": "disabled" | |
| + }, | |
| + "collection": { | |
| + "container": "DTTT_dropdown dropdown-menu", | |
| + "buttons": { | |
| + "normal": "", | |
| + "disabled": "disabled" | |
| + } | |
| + }, | |
| + "print": { | |
| + "info": "DTTT_print_info modal" | |
| + }, | |
| + "select": { | |
| + "row": "active" | |
| + } | |
| + } ); | |
| + | |
| + // Have the collection use a bootstrap compatible dropdown | |
| + $.extend( true, $.fn.DataTable.TableTools.DEFAULTS.oTags, { | |
| + "collection": { | |
| + "container": "ul", | |
| + "button": "li", | |
| + "liner": "a" | |
| + } | |
| + } ); | |
| +} | |
| diff --git a/app/assets/javascripts/jobs/view_results.coffee b/app/assets/javas… | |
| @@ -0,0 +1,52 @@ | |
| +jQuery ($) -> | |
| + $ -> | |
| + resultsPath = $('#results-path').html() | |
| + $resultsTable = $('#results-table') | |
| + | |
| + # Enable DataTable for the results list. | |
| + $resultsDataTable = $resultsTable.table | |
| + analysisTab: true | |
| + controlBarLocation: $('.analysis-control-bar') | |
| + searchInputHint: 'Search Calls' | |
| + searchable: true | |
| + datatableOptions: | |
| + "sDom": "<'row'<'span6'l><'span6'f>r>t<'row'<'span6'i><'span6'p>>", | |
| + "sPaginationType": "bootstrap", | |
| + "oLanguage": | |
| + "sEmptyTable": "No results for this job." | |
| + "sAjaxSource": resultsPath | |
| + "aaSorting": [[1, 'asc']] | |
| + "aoColumns": [ | |
| + {"mDataProp": "checkbox", "bSortable": false} | |
| + {"mDataProp": "number"} | |
| + {"mDataProp": "caller_id"} | |
| + {"mDataProp": "provider"} | |
| + {"mDataProp": "answered"} | |
| + {"mDataProp": "busy"} | |
| + {"mDataProp": "audio_length"} | |
| + {"mDataProp": "ring_length"} | |
| + ] | |
| + | |
| + # Gray out the table during loads. | |
| + $("#results-table_processing").watch 'visibility', -> | |
| + if $(this).css('visibility') == 'visible' | |
| + $resultsTable.css opacity: 0.6 | |
| + else | |
| + $resultsTable.css opacity: 1 | |
| + | |
| + # Display the search bar when the search icon is clicked | |
| + $('.button .search').click (e) -> | |
| + $filter = $('.dataTables_filter') | |
| + $input = $('.dataTables_filter input') | |
| + if $filter.css('bottom').charAt(0) == '-' # if (css matches -42px) | |
| + # input box is visible, hide it | |
| + # only allow user to hide if there is no search string | |
| + if !$input.val() || $input.val().length < 1 | |
| + $filter.css('bottom', '99999999px') | |
| + else # input box is invisible, display it | |
| + $filter.css('bottom', '-42px') | |
| + $input.focus() # auto-focus input | |
| + e.preventDefault() | |
| + | |
| + searchVal = $('.dataTables_filter input').val() | |
| + $('.button .search').click() if searchVal && searchVal.length > 0 | |
| diff --git a/app/assets/javascripts/jquery.table.coffee b/app/assets/javascript… | |
| @@ -0,0 +1,215 @@ | |
| +# table plugin | |
| +# | |
| +# Adds sorting and other dynamic functions to tables. | |
| +jQuery ($) -> | |
| + $.table = | |
| + defaults: | |
| + searchable: true | |
| + searchInputHint: 'Search' | |
| + sortableClass: 'sortable' | |
| + setFilteringDelay: false | |
| + datatableOptions: | |
| + "bStateSave": true | |
| + "oLanguage": | |
| + "sSearch": "" | |
| + "sProcessing": "Loading..." | |
| + "fnDrawCallback": -> | |
| + $.table.controlBar.buttons.enable() | |
| + "sDom": '<"control-bar"f><"list-table-header clearfix"l>t<"list-table-… | |
| + "sPaginationType": "full_numbers" | |
| + "fnInitComplete": (oSettings, json) -> | |
| + # if old search term saved, display it | |
| + searchTerm = getParameterByName 'search' | |
| + # FIX ME | |
| + $searchBox = $('#search', $(this).parents().eq(3)) | |
| + | |
| + if searchTerm | |
| + $searchBox.val searchTerm | |
| + $searchBox.focus() | |
| + | |
| + # insert the cancel button to the left of the search box | |
| + $searchBox.before('<a class="cancel-search" href="#"></a>') | |
| + $a = $('.cancel-search') | |
| + table = this | |
| + searchTerm = $searchBox.val() | |
| + searchBox = $searchBox.eq(0) | |
| + $a.hide() if (!searchTerm || searchTerm.length < 1) | |
| + | |
| + $a.click (e) -> # called when red X is clicked | |
| + $(this).hide() | |
| + table.fnFilter '' | |
| + $(searchBox).blur() # blur to trigger filler text | |
| + e.preventDefault() # Other control code can be found in… | |
| + # bind to fnFilter() calls | |
| + # do this by saving fnFilter to fnFilterOld & overriding | |
| + table['fnFilterOld'] = table.fnFilter | |
| + table.fnFilter = (str) -> | |
| + $a = jQuery('.cancel-search') | |
| + if str && str.length > 0 | |
| + $a.show() | |
| + else | |
| + $a.hide() | |
| + table.fnFilterOld(str) | |
| + | |
| + window.setTimeout ( => | |
| + this.fnFilter(searchTerm) | |
| + ), 0 | |
| + | |
| + $('.button a.search').click() if searchTerm | |
| + | |
| + analysisTabOptions: | |
| + "aLengthMenu": [[10, 50, 100, 250, 500, -1], [10, 50, 100, 250, 5… | |
| + "iDisplayLength": 10 | |
| + "bProcessing": true | |
| + "bServerSide": true | |
| + "bSortMulti": false | |
| + | |
| + checkboxes: | |
| + bind: -> | |
| + # TODO: This and any other 'table.list' selectors that appear in the p… | |
| + # code will trigger all sortable tables visible on the page. | |
| + $("table.list thead tr th input[type='checkbox']").live 'click', (e) -> | |
| + $checkboxes = $("input[type='checkbox']", "table.list tbody tr td:nt… | |
| + if $(this).attr 'checked' | |
| + $checkboxes.attr 'checked', true | |
| + else | |
| + $checkboxes.attr 'checked', false | |
| + | |
| + controlBar: | |
| + buttons: | |
| + # Disables/enables buttons based on number of checkboxes selected, | |
| + # and the class name. | |
| + enable: -> | |
| + numChecked = $("tbody tr td input[type='checkbox']", "table.list").f… | |
| + disable = ($button) -> | |
| + $button.addClass 'disabled' | |
| + $button.children('input').attr 'disabled', 'disabled' | |
| + enable = ($button) -> | |
| + $button.removeClass 'disabled' | |
| + $button.children('input').removeAttr 'disabled' | |
| + | |
| + switch numChecked | |
| + when 0 | |
| + disable $('.btn.single', '.control-bar') | |
| + disable $('.btn.multiple','.control-bar') | |
| + disable $('.btn.any', '.control-bar') | |
| + when 1 | |
| + enable $('.btn.single', '.control-bar') | |
| + disable $('.btn.multiple','.control-bar') | |
| + enable $('.btn.any', '.control-bar') | |
| + else | |
| + disable $('.btn.single', '.control-bar') | |
| + enable $('.btn.multiple','.control-bar') | |
| + enable $('.btn.any', '.control-bar') | |
| + | |
| + show: | |
| + bind: -> | |
| + # Show button | |
| + $showButton = $('span.button a.show', '.control-bar') | |
| + if $showButton.length | |
| + $showButton.click (e) -> | |
| + unless $showButton.parent('span').hasClass 'disabled' | |
| + $("table.list tbody tr td input[type='checkbox']").filter(':… | |
| + hostHref = $("table.list tbody tr td input[type='checkbox']") | |
| + .filter(':checked') | |
| + .parents('tr') | |
| + .children('td:nth-child(2)') | |
| + .children('a') | |
| + .attr('href') | |
| + window.location = hostHref | |
| + e.preventDefault() | |
| + | |
| + edit: | |
| + bind: -> | |
| + # Settings button | |
| + $editButton = $('span.button a.edit', '.control-bar') | |
| + if $editButton.length | |
| + $editButton.click (e) -> | |
| + unless $editButton.parent('span').hasClass 'disabled' | |
| + $("table.list tbody tr td input[type='checkbox']").filter(':… | |
| + hostHref = $("table.list tbody tr td input[type='checkbox']") | |
| + .filter(':checked') | |
| + .parents('tr') | |
| + .children('td:nth-child(2)') | |
| + .children('span.settings-url') | |
| + .html() | |
| + window.location = hostHref | |
| + e.preventDefault() | |
| + | |
| + bind: (options) -> | |
| + # Move the buttons into the control bar. | |
| + $('.control-bar').prepend($('.control-bar-items').html()) | |
| + $('.control-bar-items').remove() | |
| + | |
| + # Move the control bar to a new location, if specified. | |
| + if !!options.controlBarLocation | |
| + $('.control-bar').appendTo(options.controlBarLocation) | |
| + | |
| + this.enable() | |
| + this.show.bind() | |
| + this.edit.bind() | |
| + | |
| + bind: (options) -> | |
| + this.buttons.bind(options) | |
| + # Redraw the buttons with each checkbox click. | |
| + $("input[type='checkbox']", "table.list").live 'click', (e) => | |
| + this.buttons.enable() | |
| + | |
| + searchField: | |
| + # Add an input hint to the search field. | |
| + addInputHint: (options, $table) -> | |
| + if options.searchable | |
| + # if the searchbar is in a control bar, expand selector scope to inc… | |
| + searchScope = $table.parents().eq(3) if !!options.controlBarLocation | |
| + searchScope ||= $table.parents().eq(2) # otherwise limit scope to j… | |
| + $searchInput = $('.dataTables_filter input', searchScope) | |
| + # We'll need this id set for the checkbox functions. | |
| + $searchInput.attr 'id', 'search' | |
| + $searchInput.attr 'placeholder', options.searchInputHint | |
| + # $searchInput.inputHint() | |
| + | |
| + bind: ($table, options) -> | |
| + $tbody = $table.children('tbody') | |
| + dataTable = null | |
| + # Turn the table into a DataTable. | |
| + if $table.hasClass options.sortableClass | |
| + # Don't mess with the search input if there's no control bar. | |
| + unless $('.control-bar-items').length | |
| + options.datatableOptions["sDom"] = '<"list-table-header clearfix"lfr… | |
| + | |
| + datatableOptions = options.datatableOptions | |
| + # If we're loading under the Analysis tab, then load the standard | |
| + # Analysis tab options. | |
| + if options.analysisTab | |
| + $.extend(datatableOptions, options.analysisTabOptions) | |
| + options.setFilteringDelay = true | |
| + options.controlBarLocation = $('.analysis-control-bar') | |
| + | |
| + dataTable = $table.dataTable(datatableOptions) | |
| + $table.data('dataTableObject', dataTable) | |
| + dataTable.fnSetFilteringDelay(500) if options.setFilteringDelay | |
| + | |
| + # If we're loading under the Analysis tab, then load the standard Anal… | |
| + if options.analysisTab | |
| + # Gray out the table during loads. | |
| + $("##{$table.attr('id')}_processing").watch 'visibility', -> | |
| + if $(this).css('visibility') == 'visible' | |
| + $table.css opacity: 0.6 | |
| + else | |
| + $table.css opacity: 1 | |
| + | |
| + # Checking a host_ids checkbox should also check the invisible relat… | |
| + $table.find('tbody tr td input[type=checkbox].hosts').live 'change',… | |
| + $(this).siblings('input[type=checkbox]').attr('checked', $(this).a… | |
| + | |
| + this.checkboxes.bind() | |
| + this.controlBar.bind(options) | |
| + # Add an input hint to the search field. | |
| + this.searchField.addInputHint(options, $table) | |
| + # Keep width at 100%. | |
| + $table.css('width', '100%') | |
| + | |
| + $.fn.table = (options) -> | |
| + settings = $.extend true, {}, $.table.defaults, options | |
| + $table = $(this) | |
| + return this.each -> $.table.bind($table, settings) | |
| diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/styleshee… | |
| @@ -1,12 +0,0 @@ | |
| -/* | |
| - *= require bootstrap_and_overrides | |
| - */ | |
| - | |
| -/* | |
| - *= require_self | |
| - *= require formtastic | |
| - *= require formtastic-bootstrap | |
| - *= require formtastic-overrides | |
| - *= require bootstrap-lightbox | |
| - *= require dataTables/jquery.dataTables.bootstrap | |
| -*/ | |
| diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/style… | |
| @@ -0,0 +1,74 @@ | |
| +/* | |
| + *= require bootstrap_and_overrides | |
| + */ | |
| + | |
| +/* | |
| + *= require_self | |
| + *= require formtastic | |
| + *= require formtastic-bootstrap | |
| + *= require formtastic-overrides | |
| + *= require bootstrap-lightbox | |
| + *= require dataTables/jquery.dataTables.bootstrap | |
| +*/ | |
| + | |
| + | |
| +table.list { | |
| + td.actions { | |
| + vertical-align: middle; | |
| + } | |
| + | |
| + td.dataTables_empty { | |
| + text-shadow: none !important; | |
| + } | |
| + | |
| + td a.datatables-search { | |
| + color: blue; | |
| + | |
| + &:hover{ | |
| + text-decoration: underline; | |
| + } | |
| + } | |
| + thead tr { | |
| + background-size: 100% 100%; | |
| + background-color: #eeeeee; | |
| + } | |
| +} | |
| + | |
| +.dataTables_filter { | |
| + padding: 0px; | |
| + width: auto !important; | |
| + | |
| + input { | |
| + background-image: url(<%= asset_path 'search.png' %>); | |
| + background-position: 160px 6px; | |
| + background-repeat: no-repeat; | |
| + height: 18px; | |
| + padding-left: 5px; | |
| + width: 170px; | |
| + } | |
| +} | |
| + | |
| +.dataTables_info { | |
| + font-size: 11px; | |
| + font-color: #666666; | |
| +} | |
| + | |
| +.dataTables_length label { | |
| + font-weight: bold; | |
| +} | |
| + | |
| +.dataTables_length select { | |
| + font-weight: bold; | |
| +} | |
| + | |
| +.control-bar { | |
| + padding: 5px; | |
| + text-align: center; | |
| +} | |
| + | |
| +.control-bar table { | |
| + width: 320px; | |
| + border: 0; | |
| + margin-left: auto; | |
| + margin-right: auto; | |
| +} | |
| diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/asse… | |
| @@ -38,6 +38,28 @@ body { | |
| @navbarBackground: #ea5709; | |
| @navbarBackgroundHighlight: #4A1C04; | |
| + | |
| +// Datatables | |
| + | |
| +.paginate_disabled_previous { | |
| + display: none; | |
| +} | |
| + | |
| +.paginate_disabled_next { | |
| + display: none; | |
| +} | |
| + | |
| +.paginate_enabled_previous { | |
| + color: red; | |
| + margin-right: 20px; | |
| +} | |
| + | |
| +.paginate_enabled_next { | |
| + color: green; | |
| +} | |
| + | |
| +// End of DataTables | |
| + | |
| .call-detail { | |
| font-size: 10px; | |
| } | |
| diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controll… | |
| @@ -1,5 +1,7 @@ | |
| class JobsController < ApplicationController | |
| + require 'shellwords' | |
| + | |
| def index | |
| @reload_interval = 20000 | |
| @@ -42,11 +44,6 @@ class JobsController < ApplicationController | |
| def view_results | |
| @job = Job.find(params[:id]) | |
| - @results = @job.calls.paginate( | |
| - :page => params[:page], | |
| - :order => 'number ASC', | |
| - :per_page => 30 | |
| - ) | |
| @call_results = { | |
| :Timeout => @job.calls.count(:conditions => { :answered => fa… | |
| @@ -54,6 +51,78 @@ class JobsController < ApplicationController | |
| :Answered => @job.calls.count(:conditions => { :answered => tr… | |
| } | |
| + sort_by = params[:sort_by] || 'number' | |
| + sort_dir = params[:sort_dir] || 'asc' | |
| + | |
| + @results = [] | |
| + @results_total_count = @job.calls.count() | |
| + | |
| + if request.format.json? | |
| + if params[:iDisplayLength] == '-1' | |
| + @results_per_page = nil | |
| + else | |
| + @results_per_page = (params[:iDisplayLength] || 20).to_i | |
| + end | |
| + @results_offset = (params[:iDisplayStart] || 0).to_i | |
| + | |
| + calls_search | |
| + @results = @job.calls.includes(:provider).where(@search_conditions).limi… | |
| + @results_total_display_count = @job.calls.includes(:provider).where(@sea… | |
| + end | |
| + | |
| + respond_to do |format| | |
| + format.html | |
| + format.json { render :partial => 'view_results', :results => @results, :… | |
| + end | |
| + end | |
| + | |
| + # Generate a SQL sort by option based on the incoming DataTables paramater. | |
| + # | |
| + # Returns the SQL String. | |
| + def calls_sort_option | |
| + column = case params[:iSortCol_0].to_s | |
| + when '1' | |
| + 'number' | |
| + when '2' | |
| + 'caller_id' | |
| + when '3' | |
| + 'providers.name' | |
| + when '4' | |
| + 'answered' | |
| + when '5' | |
| + 'busy' | |
| + when '6' | |
| + 'audio_length' | |
| + when '7' | |
| + 'ring_length' | |
| + end | |
| + column + ' ' + (params[:sSortDir_0] =~ /^A/i ? 'asc' : 'desc') if column | |
| + end | |
| + | |
| + def calls_search | |
| + @search_conditions = [] | |
| + terms = params[:sSearch].to_s | |
| + terms = Shellword.shellwords(terms) rescue terms.split(/\s+/) | |
| + where = "" | |
| + param = [] | |
| + glue = "" | |
| + terms.each do |w| | |
| + where << glue | |
| + case w | |
| + when 'answered' | |
| + where << "answered = ? " | |
| + param << true | |
| + when 'busy' | |
| + where << "busy = ? " | |
| + param << true | |
| + else | |
| + where << "( number ILIKE ? OR caller_id ILIKE … | |
| + param << "%#{w}%" | |
| + param << "%#{w}%" | |
| + end | |
| + glue = "AND " if glue.empty? | |
| + @search_conditions = [ where, *param ] | |
| + end | |
| end | |
| def new_dialer | |
| @@ -64,12 +133,29 @@ class JobsController < ApplicationController | |
| @job.project = Project.last | |
| end | |
| + if params[:result_ids] | |
| + nums = "" | |
| + Call.find_each(:conditions => { :id => params[:result_ids] }) do |… | |
| + nums << call.number + "\n" | |
| + end | |
| + @job.range = nums | |
| + end | |
| + | |
| + | |
| respond_to do |format| | |
| format.html # new.html.erb | |
| format.xml { render :xml => @job } | |
| end | |
| end | |
| + def purge_calls | |
| + @job = Job.find(params[:id]) | |
| + Call.delete_all(:id => params[:result_ids]) | |
| + CallMedium.delete_all(:call_id => params[:result_ids]) | |
| + flash[:notice] = "Purged #{params[:result_ids].length} calls" | |
| + redirect_to view_results_path(@job.project_id, @job.id) | |
| + end | |
| + | |
| def dialer | |
| @job = Job.new(params[:job]) | |
| @job.created_by = current_user.login | |
| @@ -114,10 +200,21 @@ class JobsController < ApplicationController | |
| def analyze_job | |
| @job = Job.find(params[:id]) | |
| - @new = Job.new({ | |
| - :task => 'analysis', :scope => 'job', :target_id => @job.id, | |
| - :project_id => @project.id, :status => 'submitted' | |
| - }) | |
| + | |
| + # Handle analysis of specific call IDs via checkbox submission | |
| + if params[:result_ids] | |
| + @new = Job.new({ | |
| + :task => 'analysis', :scope => 'calls', :target_ids =>… | |
| + :project_id => @project.id, :status => 'submitted' | |
| + }) | |
| + else | |
| + # Otherwise analyze the entire Job | |
| + @new = Job.new({ | |
| + :task => 'analysis', :scope => 'job', :target_id => @j… | |
| + :project_id => @project.id, :status => 'submitted' | |
| + }) | |
| + end | |
| + | |
| respond_to do |format| | |
| if @new.schedule | |
| flash[:notice] = 'Analysis job was successfully created.' | |
| diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper… | |
| @@ -92,6 +92,159 @@ module ApplicationHelper | |
| else | |
| job.status.to_s.capitalize | |
| end | |
| + end | |
| + | |
| + # | |
| + # Includes any javascripts specific to this view. The hosts/show view | |
| + # will automatically include any javascripts at public/javascripts/hos… | |
| + # | |
| + # @return [void] | |
| + def include_view_javascript | |
| + # | |
| + # Sprockets treats index.js as special, so the js for the inde… | |
| + # http://guides.rubyonrails.org/asset_pipeline.html#using-inde… | |
| + # | |
| + | |
| + controller_action_name = controller.action_name | |
| + | |
| + if controller_action_name == 'index' | |
| + safe_action_name = '_index' | |
| + else | |
| + safe_action_name = controller_action_name | |
| + end | |
| + | |
| + include_view_javascript_named(safe_action_name) | |
| + end | |
| + | |
| + # Includes the named javascript for this controller if it exists. | |
| + # | |
| + # @return [void] | |
| + def include_view_javascript_named(name) | |
| + | |
| + controller_path = controller.controller_path | |
| + extensions = ['.coffee', '.js.coffee'] | |
| + javascript_controller_pathname = Rails.root.join('app', 'asset… | |
| + pathnames = extensions.collect { |extension| | |
| + javascript_controller_pathname.join("#{name}#{extensio… | |
| + } | |
| + | |
| + if pathnames.any?(&:exist?) | |
| + path = File.join(controller_path, name) | |
| + content_for(:view_javascript) do | |
| + javascript_include_tag path | |
| + end | |
| + end | |
| + end | |
| + | |
| + | |
| + | |
| + # | |
| + # Generate pagination links | |
| + # | |
| + # Parameters: | |
| + # :name:: the kind of the items we're paginating | |
| + # :items:: the collection of items currently on the page | |
| + # :count:: total count of items to paginate | |
| + # :offset:: offset from the beginning where +items+ starts within the total | |
| + # :page:: current page | |
| + # :num_pages:: total number of pages | |
| + # | |
| + def page_links(opts={}) | |
| + link_method = opts[:link_method] | |
| + if not link_method or not respond_to? link_method | |
| + raise RuntimeError.new("Need a method for generating links") | |
| + end | |
| + name = opts[:name] || "" | |
| + items = opts[:items] || [] | |
| + count = opts[:count] || 0 | |
| + offset = opts[:offset] || 0 | |
| + page = opts[:page] || 1 | |
| + num_pages = opts[:num_pages] || 1 | |
| + | |
| + page_list = "" | |
| + 1.upto(num_pages) do |p| | |
| + if p == page | |
| + page_list << content_tag(:span, :class=>"current") { h page } | |
| + else | |
| + page_list << self.send(link_method, p, { :page => p }) | |
| + end | |
| + end | |
| + content_tag(:div, :id => "page_links") do | |
| + content_tag(:span, :class => "index") do | |
| + if items.size > 0 | |
| + "#{offset + 1}-#{offset + items.size} of #{h pluralize(count, name)}… | |
| + else | |
| + h(name.pluralize) | |
| + end.html_safe | |
| + end + | |
| + if num_pages > 1 | |
| + self.send(link_method, '', { :page => 0 }, { :class => 'start' }) + | |
| + self.send(link_method, '', { :page => page-1 }, {:class => 'prev' … | |
| + page_list + | |
| + self.send(link_method, '', { :page => [page+1,num_pages].min }, { … | |
| + self.send(link_method, '', { :page => num_pages }, { :class => 'en… | |
| + else | |
| + "" | |
| + end | |
| + end | |
| + end | |
| + | |
| + def submit_checkboxes_to(name, path, html={}) | |
| + if html[:confirm] | |
| + confirm = html.delete(:confirm) | |
| + link_to(name, "#", html.merge({:onclick => "if(confirm… | |
| + else | |
| + link_to(name, "#", html.merge({:onclick => "submit_che… | |
| + end | |
| + end | |
| + | |
| + # Scrub out data that can break the JSON parser | |
| + # | |
| + # data - The String json to be scrubbed. | |
| + # | |
| + # Returns the String json with invalid data removed. | |
| + def json_data_scrub(data) | |
| + data.to_s.gsub(/[\x00-\x1f]/){ |x| "\\x%.2x" % x.unpack("C*")[… | |
| + end | |
| + # Returns the properly escaped sEcho parameter that DataTables expects. | |
| + def echo_data_tables | |
| + h(params[:sEcho]).to_json.html_safe | |
| end | |
| + | |
| + # Generate the markup for the call's row checkbox. | |
| + # Returns the String markup html, escaped for json. | |
| + def call_checkbox_tag(call) | |
| + check_box_tag("result_ids[]", call.id, false, :id => nil).to_j… | |
| + end | |
| + | |
| + def call_number_html(call) | |
| + json_data_scrub(h(call.number)).to_json.html_safe | |
| + end | |
| + | |
| + def call_caller_id_html(call) | |
| + json_data_scrub(h(call.caller_id)).to_json.html_safe | |
| + end | |
| + | |
| + def call_provider_html(call) | |
| + json_data_scrub(h(call.provider.name)).to_json.html_safe | |
| + end | |
| + | |
| + def call_answered_html(call) | |
| + json_data_scrub(h(call.answered ? "Yes" : "No")).to_json.html_… | |
| + end | |
| + | |
| + def call_busy_html(call) | |
| + json_data_scrub(h(call.busy ? "Yes" : "No")).to_json.html_safe | |
| + end | |
| + | |
| + def call_audio_length_html(call) | |
| + json_data_scrub(h(call.audio_length.to_s)).to_json.html_safe | |
| + end | |
| + | |
| + def call_ring_length_html(call) | |
| + json_data_scrub(h(call.ring_lenght.to_s)).to_json.html_safe | |
| + end | |
| + | |
| + | |
| end | |
| diff --git a/app/models/job.rb b/app/models/job.rb | |
| @@ -23,8 +23,8 @@ class Job < ActiveRecord::Base | |
| record.errors[:lines] << "Lines should… | |
| end | |
| when 'analysis' | |
| - unless ['job', 'project', 'global'].include?(r… | |
| - record.errors[:scope] << "Scope must b… | |
| + unless ['calls', 'job', 'project', 'global'].i… | |
| + record.errors[:scope] << "Scope must b… | |
| end | |
| if record.scope == "job" and Job.where(:id => … | |
| record.errors[:job_id] << "The job_id … | |
| @@ -32,6 +32,9 @@ class Job < ActiveRecord::Base | |
| if record.scope == "project" and Project.where… | |
| record.errors[:project_id] << "The pro… | |
| end | |
| + if record.scope == "calls" and (record.target_… | |
| + record.errors[:target_ids] << "The tar… | |
| + end | |
| when 'import' | |
| else | |
| record.errors[:base] << "Invalid task specifie… | |
| @@ -64,8 +67,9 @@ class Job < ActiveRecord::Base | |
| attr_accessor :scope | |
| attr_accessor :force | |
| attr_accessor :target_id | |
| + attr_accessor :target_ids | |
| - attr_accessible :scope, :force, :target_id | |
| + attr_accessible :scope, :force, :target_id, :target_ids | |
| validates_with JobValidator | |
| @@ -102,7 +106,8 @@ class Job < ActiveRecord::Base | |
| self.args = Marshal.dump({ | |
| :scope => self.scope, # job / pr… | |
| :force => !!(self.force), # true / f… | |
| - :target_id => self.target_id.to_i # job_id o… | |
| + :target_id => self.target_id.to_i, # job_id o… | |
| + :target_ids => self.target_ids.map{|x| x.to_i } | |
| }) | |
| return self.save | |
| else | |
| diff --git a/app/views/jobs/_view_results.json.erb b/app/views/jobs/_view_resul… | |
| @@ -0,0 +1,20 @@ | |
| +{ | |
| + "sEcho": <%= echo_data_tables %>, | |
| + "iTotalRecords": <%= @results_total_count.to_json %>, | |
| + "iTotalDisplayRecords": <%= @results_total_display_count.to_json %>, | |
| + "aaData": [ | |
| + <% @results.each_with_index do |result, index| -%> | |
| + { | |
| + "DT_RowId": <%= dom_id(result).to_json.html_safe%>, | |
| + "checkbox": <%= call_checkbox_tag(result) %>, | |
| + "number": <%= call_number_html(result) %>, | |
| + "caller_id": <%= call_caller_id_html(result) %>, | |
| + "provider": <%= call_provider_html(result) %>, | |
| + "answered": <%= call_answered_html(result) %>, | |
| + "busy": <%= call_busy_html(result) %>, | |
| + "audio_length": <%= call_audio_length_html(result) %>, | |
| + "ring_length": <%= call_audio_length_html(result) %> | |
| + }<%= ',' unless index == (@results.size - 1) %> | |
| + <% end -%> | |
| + ] | |
| +} | |
| diff --git a/app/views/jobs/view_results.html.erb b/app/views/jobs/view_results… | |
| @@ -1,10 +1,9 @@ | |
| -<% if @results.length > 0 %> | |
| - | |
| +<% include_view_javascript %> | |
| <h1 class='title'>Call Results for Scan #<%[email protected]%></h1> | |
| -<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6> | |
| +<table class='table table-striped table-condensed'> | |
| <tr> | |
| <td align='center'> | |
| <%= render :partial => 'shared/graphs/call_results' %> | |
| @@ -12,13 +11,35 @@ | |
| </tr> | |
| </table> | |
| -<br/> | |
| -<%= will_paginate @results, :renderer => BootstrapPagination::Rails %> | |
| -<table class='table table-striped table-condensed' width='90%' id='results'> | |
| + | |
| +<%= form_tag do %> | |
| +<div class="control-bar"> | |
| +<table width='100%' border=0 cellpadding=6> | |
| +<tbody><tr> | |
| +<td> | |
| + <%= submit_checkboxes_to(raw('<i class="icon-refresh"></i> Scan'), new… | |
| +</td><td> | |
| + <%= submit_checkboxes_to(raw('<i class="icon-cog"></i> Analyze'), anal… | |
| +</td><td> | |
| + <%= submit_checkboxes_to(raw('<i class="icon-trash"></i> Delete'), pur… | |
| +</td><td> | |
| + <a class="btn btn-mini any" href="#"><i class="icon-trash"></i> Purge<… | |
| +</td> | |
| +</tr></tbody></table> | |
| + | |
| +</div> | |
| + | |
| + | |
| +<div class="analysis-control-bar"> </div> | |
| + | |
| +<span id="results-path" class="invisible"><%= view_results_path(@project, @job… | |
| + | |
| +<table id='results-table' class='table table-striped table-condensed sortable … | |
| <thead> | |
| <tr> | |
| + <th><%= check_box_tag "all_results", false %></th> | |
| <th>Number</th> | |
| <th>Source CID</th> | |
| <th>Provider</th> | |
| @@ -28,25 +49,10 @@ | |
| <th>Ring Time</th> | |
| </tr> | |
| </thead> | |
| - <tbody> | |
| -<% @results.each do |call| %> | |
| - <tr> | |
| - <td><%= call.number %></td> | |
| - <td><%= call.caller_id %></td> | |
| - <td><%= call.provider.name %></td> | |
| - <td><%= call.answered ? "Yes" : "No" %></td> | |
| - <td><%= call.busy %></td> | |
| - <td><%= call.audio_length %></td> | |
| - <td><%= call.ring_length %></td> | |
| - </tr> | |
| -<% end %> | |
| + <tbody id="results-list"> | |
| </tbody> | |
| </table> | |
| -<%= will_paginate @results, :renderer => BootstrapPagination::Rails %> | |
| - | |
| -<% else %> | |
| - | |
| -<h1 class='title'>No Results</h1> | |
| +</div> | |
| <% end %> | |
| diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/applica… | |
| @@ -1,18 +1,21 @@ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| - <meta charset="utf-8"> | |
| - <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"> | |
| - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + <meta charset="utf-8"> | |
| + <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"> | |
| + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title><%= content_for?(:title) ? yield(:title) : "WarVOX v#{WarVOX::VERSI… | |
| - <%= csrf_meta_tags %> | |
| + <%= csrf_meta_tags %> | |
| - <!--[if lt IE 9]> | |
| - <%= javascript_include_tag "html5" %> | |
| - <![endif]--> | |
| + <!--[if lt IE 9]> | |
| + <%= javascript_include_tag "html5" %> | |
| + <![endif]--> | |
| + | |
| +<%= javascript_include_tag "application" %> | |
| +<%= yield :view_javascript %> | |
| +<%= stylesheet_link_tag "application", :media => "all" %> | |
| +<%= yield :view_stylesheets %> | |
| - <%= javascript_include_tag "application" %> | |
| - <%= stylesheet_link_tag "application", :media => "all" %> | |
| <%= favicon_link_tag '/assets/apple-touch-icon-144x144-precomposed.png', :… | |
| <%= favicon_link_tag '/assets/apple-touch-icon-114x114-precomposed.png', :… | |
| @@ -90,7 +93,6 @@ | |
| <% end %> | |
| <% end %> | |
| - | |
| <div class="row"> | |
| <div class="span12 content"> | |
| <div class="content"> | |
| diff --git a/config/environments/development.rb b/config/environments/developme… | |
| @@ -25,15 +25,17 @@ Web::Application.configure do | |
| # Raise exception on mass assignment protection for Active Record models | |
| config.active_record.mass_assignment_sanitizer = :strict | |
| + config.log_level = :debug | |
| + | |
| # Log the query plan for queries taking more than this (works | |
| # with SQLite, MySQL, and PostgreSQL) | |
| - config.active_record.auto_explain_threshold_in_seconds = 0.5 | |
| + config.active_record.auto_explain_threshold_in_seconds = 0.75 | |
| # Do not compress assets | |
| config.assets.compress = false | |
| # Expands the lines which load the assets | |
| - config.assets.debug = true | |
| + config.assets.debug = false | |
| config.serve_static_assets = true | |
| end | |
| diff --git a/config/routes.rb b/config/routes.rb | |
| @@ -8,11 +8,12 @@ Web::Application.routes.draw do | |
| match '/projects/:project_id/all' => 'projects#index', :… | |
| - match '/jobs/dial' => 'jobs#new_dialer', :as => :new_dialer_job | |
| - match '/jobs/dialer' => 'jobs#dialer', :as => :dialer_job | |
| - match '/jobs/analyze' => 'jobs#new_analyzer', :as => :new_analyzer_job | |
| - match '/jobs/analyzer' => 'jobs#analyzer', :as => :analyzer_job | |
| - match '/jobs/:id/stop' => 'jobs#stop', :as => :stop_job | |
| + match '/jobs/dial' => 'jobs#new_dialer', :as => :new_dialer_job | |
| + match '/jobs/dialer' => 'jobs#dialer', :as => :dialer_job | |
| + match '/jobs/analyze' => 'jobs#new_analyzer', :as => :new_analyzer_… | |
| + match '/jobs/analyzer' => 'jobs#analyzer', :as => :analyzer_job | |
| + match '/jobs/:id/stop' => 'jobs#stop', :as => :stop_job | |
| + match '/jobs/:id/calls/purge' => "jobs#purge_calls", :as => :purge_calls_j… | |
| match '/projects/:project_id/scans' => 'jobs#results', :as => :res… | |
| match '/projects/:project_id/scans/:id' => 'jobs#view_results', :as =>… | |
| diff --git a/db/migrate/20130106000000_add_indexes.rb b/db/migrate/201301060000… | |
| @@ -0,0 +1,29 @@ | |
| +class AddIndexes < ActiveRecord::Migration | |
| + def up | |
| + add_index :jobs, :project_id | |
| + add_index :lines, :number | |
| + add_index :lines, :project_id | |
| + add_index :line_attributes, :line_id | |
| + add_index :line_attributes, :project_id | |
| + add_index :calls, :number | |
| + add_index :calls, :job_id | |
| + add_index :calls, :provider_id | |
| + add_index :call_media, :call_id | |
| + add_index :call_media, :project_id | |
| + add_index :signature_fp, :signature_id | |
| + end | |
| + | |
| + def down | |
| + remove_index :jobs, :project_id | |
| + remove_index :lines, :number | |
| + remove_index :lines, :project_id | |
| + remove_index :line_attributes, :line_id | |
| + remove_index :line_attributes, :project_id | |
| + remove_index :calls, :number | |
| + remove_index :calls, :job_id | |
| + remove_index :calls, :provider_id | |
| + remove_index :call_media, :call_id | |
| + remove_index :call_media, :project_id | |
| + remove_index :signature_fp, :signature_id | |
| + end | |
| +end | |
| diff --git a/db/schema.rb b/db/schema.rb | |
| @@ -11,7 +11,7 @@ | |
| # | |
| # It's strongly recommended to check this file into your version control syste… | |
| -ActiveRecord::Schema.define(:version => 20121228171549) do | |
| +ActiveRecord::Schema.define(:version => 20130106000000) do | |
| add_extension "intarray" | |
| @@ -27,6 +27,9 @@ ActiveRecord::Schema.define(:version => 20121228171549) do | |
| t.binary "png_sig_freq" | |
| end | |
| + add_index "call_media", ["call_id"], :name => "index_call_media_on_call_id" | |
| + add_index "call_media", ["project_id"], :name => "index_call_media_on_projec… | |
| + | |
| create_table "calls", :force => true do |t| | |
| t.datetime "created_at", :null => false | |
| t.datetime "updated_at", :null => false | |
| @@ -49,6 +52,10 @@ ActiveRecord::Schema.define(:version => 20121228171549) do | |
| t.integer "fprint", :array => true | |
| end | |
| + add_index "calls", ["job_id"], :name => "index_calls_on_job_id" | |
| + add_index "calls", ["number"], :name => "index_calls_on_number" | |
| + add_index "calls", ["provider_id"], :name => "index_calls_on_provider_id" | |
| + | |
| create_table "jobs", :force => true do |t| | |
| t.datetime "created_at", :null => false | |
| t.datetime "updated_at", :null => false | |
| @@ -65,6 +72,8 @@ ActiveRecord::Schema.define(:version => 20121228171549) do | |
| t.integer "progress", :default => 0 | |
| end | |
| + add_index "jobs", ["project_id"], :name => "index_jobs_on_project_id" | |
| + | |
| create_table "line_attributes", :force => true do |t| | |
| t.datetime "created_at", :null => false | |
| t.datetime "updated_at", :null => false | |
| @@ -75,6 +84,9 @@ ActiveRecord::Schema.define(:version => 20121228171549) do | |
| t.string "content_type", :default => "text" | |
| end | |
| + add_index "line_attributes", ["line_id"], :name => "index_line_attributes_on… | |
| + add_index "line_attributes", ["project_id"], :name => "index_line_attributes… | |
| + | |
| create_table "lines", :force => true do |t| | |
| t.datetime "created_at", :null => false | |
| t.datetime "updated_at", :null => false | |
| @@ -84,6 +96,9 @@ ActiveRecord::Schema.define(:version => 20121228171549) do | |
| t.text "notes" | |
| end | |
| + add_index "lines", ["number"], :name => "index_lines_on_number" | |
| + add_index "lines", ["project_id"], :name => "index_lines_on_project_id" | |
| + | |
| create_table "projects", :force => true do |t| | |
| t.datetime "created_at", :null => false | |
| t.datetime "updated_at", :null => false | |
| @@ -122,6 +137,8 @@ ActiveRecord::Schema.define(:version => 20121228171549) do | |
| t.integer "fprint", :array => true | |
| end | |
| + add_index "signature_fp", ["signature_id"], :name => "index_signature_fp_on_… | |
| + | |
| create_table "signatures", :force => true do |t| | |
| t.datetime "created_at", :null => false | |
| t.datetime "updated_at", :null => false | |
| diff --git a/lib/warvox/jobs/analysis.rb b/lib/warvox/jobs/analysis.rb | |
| @@ -65,11 +65,11 @@ class Analysis < Base | |
| end | |
| case @conf[:scope] | |
| - when 'call' | |
| + when 'calls': | |
| if @conf[:force] | |
| - query = {:id => @conf[:target_id], :answered =… | |
| + query = {:id => @conf[:target_ids], :answered … | |
| else | |
| - query = {:id => @conf[:target_id], :answered =… | |
| + query = {:id => @conf[:target_ids], :answered … | |
| end | |
| when 'job' | |
| if @conf[:force] | |
| @@ -89,6 +89,9 @@ class Analysis < Base | |
| else | |
| query = {:answered => true, :busy => false, :a… | |
| end | |
| + else | |
| + # Bail if we don't have a valid scope | |
| + return | |
| end | |
| # Build a list of call IDs, as find_each() gets confused if th… |