Sync up recent work - warvox - VoIP based wardialing tool, forked from rapid7/warvox.
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
---
(DIR) commit 6fba0686fb93cf3157e42be1dff58a5b9cace5b0
(DIR) parent 876a89b6d4cde9a8e6df003f1aa0c08565040afc
(HTM) Author: HD Moore <hd_moore@rapid7.com>
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(-)
---
(DIR) diff --git a/app/assets/images/search.png b/app/assets/images/search.png
Binary files differ.
(DIR) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
@@ -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].type.toLowerCase() != 'hidden' && fields[i].type.toLowerCase() != 'submit') {
+ 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[i].type.toLowerCase() != 'hidden') {
+ 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"]')[0];
+
+ 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');
+ });
+}
(DIR) diff --git a/app/assets/javascripts/dataTables.filteringDelay.js b/app/assets/javascripts/dataTables.filteringDelay.js
@@ -0,0 +1,44 @@
+jQuery.fn.dataTableExt.oApi.fnSetFilteringDelay = function ( oSettings, iDelay ) {
+ /*
+ * Inputs: object:oSettings - dataTables settings object - automatically given
+ * integer:iDelay - delay in milliseconds
+ * Usage: $('#example').dataTable().fnSetFilteringDelay(250);
+ * Author: Zygimantas Berziunas (www.zygimantas.com) and Allan Jardine
+ * 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().aanFeatures.f );
+
+ anControl.unbind( 'keyup' ).bind( 'keyup', function() {
+ var $$this = $this;
+
+ if (sPreviousSearch === null || sPreviousSearch != anControl.val()) {
+ 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();
+} ); */
+
(DIR) diff --git a/app/assets/javascripts/dataTables.fnReloadAjax.js b/app/assets/javascripts/dataTables.fnReloadAjax.js
@@ -0,0 +1,53 @@
+jQuery.fn.dataTableExt.oApi.fnReloadAjax = function ( oSettings, sNewSource, fnCallback, bStandingRedraw )
+{
+ 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, aData, function(json) {
+ /* 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
(DIR) diff --git a/app/assets/javascripts/dataTables.hiddenTitle.js b/app/assets/javascripts/dataTables.hiddenTitle.js
@@ -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));
+};
(DIR) diff --git a/app/assets/javascripts/dataTables_overrides.js b/app/assets/javascripts/dataTables_overrides.js
@@ -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 / oSettings._iDisplayLength ),
+ "iTotalPages": Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength )
+ };
+};
+
+/* 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.data.action) ) {
+ fnDraw( oSettings );
+ }
+ };
+
+ $(nPaging).addClass('pagination').append(
+ '<ul>'+
+ '<li class="prev disabled"><a href="#">← '+oLang.sPrevious+'</a></li>'+
+ '<li class="next disabled"><a href="#">'+oLang.sNext+' → </a></li>'+
+ '</ul>'
+ );
+ var els = $('a', nPaging);
+ $(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler );
+ $(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler );
+ },
+
+ "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(iListLength/2);
+
+ if ( oPaging.iTotalPages < iListLength) {
+ iStart = 1;
+ iEnd = oPaging.iTotalPages;
+ }
+ else if ( oPaging.iPage <= iHalf ) {
+ iStart = 1;
+ iEnd = iListLength;
+ } else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) {
+ 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)').remove();
+
+ // Add the new list items and their event handlers
+ for ( j=iStart ; j<=iEnd ; j++ ) {
+ sClass = (j==oPaging.iPage+1) ? 'class="active"' : '';
+ $('<li '+sClass+'><a href="#">'+j+'</a></li>')
+ .insertBefore( $('li:last', an[i])[0] )
+ .bind('click', function (e) {
+ e.preventDefault();
+ oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength;
+ fnDraw( oSettings );
+ } );
+ }
+
+ // Add / remove disabled classes from the static elements
+ if ( oPaging.iPage === 0 ) {
+ $('li:first', an[i]).addClass('disabled');
+ } else {
+ $('li:first', an[i]).removeClass('disabled');
+ }
+
+ if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) {
+ $('li:last', an[i]).addClass('disabled');
+ } else {
+ $('li:last', an[i]).removeClass('disabled');
+ }
+ }
+ }
+ }
+} );
+
+
+/*
+ * TableTools Bootstrap compatibility
+ * Required TableTools 2.1+
+ */
+if ( $.fn.DataTable.TableTools ) {
+ // Set the classes that TableTools uses to something suitable for Bootstrap
+ $.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"
+ }
+ } );
+}
(DIR) diff --git a/app/assets/javascripts/jobs/view_results.coffee b/app/assets/javascripts/jobs/view_results.coffee
@@ -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
(DIR) diff --git a/app/assets/javascripts/jquery.table.coffee b/app/assets/javascripts/jquery.table.coffee
@@ -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-footer clearfix"ip>r'
+ "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 filteringDelay.js plugin.
+ # 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, 500, "All"]]
+ "iDisplayLength": 10
+ "bProcessing": true
+ "bServerSide": true
+ "bSortMulti": false
+
+ checkboxes:
+ bind: ->
+ # TODO: This and any other 'table.list' selectors that appear in the plugin
+ # 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:nth-child(1)")
+ 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").filter(':checked').not('.invisible').size()
+ 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(':checked').not('.invisible')
+ 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(':checked').not('.invisible')
+ 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 include control bar
+ searchScope = $table.parents().eq(3) if !!options.controlBarLocation
+ searchScope ||= $table.parents().eq(2) # otherwise limit scope to just the table
+ $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>t<"list-table-footer clearfix"ip>'
+
+ 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 Analysis tab functions.
+ 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 related object checkbox.
+ $table.find('tbody tr td input[type=checkbox].hosts').live 'change', ->
+ $(this).siblings('input[type=checkbox]').attr('checked', $(this).attr('checked'))
+
+ 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)
(DIR) diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss
@@ -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
-*/
(DIR) diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb
@@ -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;
+}
(DIR) diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less
@@ -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;
}
(DIR) diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb
@@ -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 => false }),
@@ -54,6 +51,78 @@ class JobsController < ApplicationController
:Answered => @job.calls.count(:conditions => { :answered => true }),
}
+ 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).limit(@results_per_page).offset(@results_offset).order(calls_sort_option)
+ @results_total_display_count = @job.calls.includes(:provider).where(@search_conditions).count()
+ end
+
+ respond_to do |format|
+ format.html
+ format.json { render :partial => 'view_results', :results => @results, :call_results => @call_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 |call|
+ 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 => params[:result_ids],
+ :project_id => @project.id, :status => 'submitted'
+ })
+ else
+ # Otherwise analyze the entire Job
+ @new = Job.new({
+ :task => 'analysis', :scope => 'job', :target_id => @job.id,
+ :project_id => @project.id, :status => 'submitted'
+ })
+ end
+
respond_to do |format|
if @new.schedule
flash[:notice] = 'Analysis job was successfully created.'
(DIR) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
@@ -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/hosts/show.js.
+ #
+ # @return [void]
+ def include_view_javascript
+ #
+ # Sprockets treats index.js as special, so the js for the index action must be called _index.js instead.
+ # http://guides.rubyonrails.org/asset_pipeline.html#using-index-files
+ #
+
+ 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', 'assets', 'javascripts', controller_path)
+ pathnames = extensions.collect { |extension|
+ javascript_controller_pathname.join("#{name}#{extension}")
+ }
+
+ 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)}" + " "*3
+ 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 }, { :class => 'next' }) +
+ self.send(link_method, '', { :page => num_pages }, { :class => 'end' })
+ 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('#{h confirm}')){ submit_checkboxes_to('#{path}','#{form_authenticity_token}')}else{return false;}" }))
+ else
+ link_to(name, "#", html.merge({:onclick => "submit_checkboxes_to('#{path}','#{form_authenticity_token}')" }))
+ 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*")[0] }
+ 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_json.html_safe
+ 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_safe
+ 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
(DIR) 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 be between 1 and 10,000"
end
when 'analysis'
- unless ['job', 'project', 'global'].include?(record.scope)
- record.errors[:scope] << "Scope must be job, project, or global"
+ unless ['calls', 'job', 'project', 'global'].include?(record.scope)
+ record.errors[:scope] << "Scope must be calls, job, project, or global"
end
if record.scope == "job" and Job.where(:id => record.target_id.to_i, :task => ['import', 'dialer']).count == 0
record.errors[:job_id] << "The job_id is not valid"
@@ -32,6 +32,9 @@ class Job < ActiveRecord::Base
if record.scope == "project" and Project.where(:id => record.target_id.to_i).count == 0
record.errors[:project_id] << "The project_id is not valid"
end
+ if record.scope == "calls" and (record.target_ids.nil? or record.target_ids.length == 0)
+ record.errors[:target_ids] << "The target_ids list is empty"
+ end
when 'import'
else
record.errors[:base] << "Invalid task specified"
@@ -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 / project/ global
:force => !!(self.force), # true / false
- :target_id => self.target_id.to_i # job_id or project_id or nil
+ :target_id => self.target_id.to_i, # job_id or project_id or nil
+ :target_ids => self.target_ids.map{|x| x.to_i }
})
return self.save
else
(DIR) diff --git a/app/views/jobs/_view_results.json.erb b/app/views/jobs/_view_results.json.erb
@@ -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 -%>
+ ]
+}
(DIR) diff --git a/app/views/jobs/view_results.html.erb b/app/views/jobs/view_results.html.erb
@@ -1,10 +1,9 @@
-<% if @results.length > 0 %>
-
+<% include_view_javascript %>
<h1 class='title'>Call Results for Scan #<%=@job.id%></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_dialer_job_path, { :class => "btn btn-mini any" }) %>
+</td><td>
+ <%= submit_checkboxes_to(raw('<i class="icon-cog"></i> Analyze'), analyze_job_path, { :class => "btn btn-mini any" }) %>
+</td><td>
+ <%= submit_checkboxes_to(raw('<i class="icon-trash"></i> Delete'), purge_calls_job_path, { :class => "btn btn-mini any", :confirm => 'Purge selected calls?' }) %>
+</td><td>
+ <a class="btn btn-mini any" href="#"><i class="icon-trash"></i> Purge</button>
+</td>
+</tr></tbody></table>
+
+</div>
+
+
+<div class="analysis-control-bar"> </div>
+
+<span id="results-path" class="invisible"><%= view_results_path(@project, @job.id, :format => :json) %></span>
+
+<table id='results-table' class='table table-striped table-condensed sortable list' >
<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 %>
(DIR) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
@@ -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::VERSION}" %></title>
- <%= 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', :rel => 'apple-touch-icon-precomposed', :type => 'image/png', :sizes => '144x144' %>
<%= favicon_link_tag '/assets/apple-touch-icon-114x114-precomposed.png', :rel => 'apple-touch-icon-precomposed', :type => 'image/png', :sizes => '114x114' %>
@@ -90,7 +93,6 @@
<% end %>
<% end %>
-
<div class="row">
<div class="span12 content">
<div class="content">
(DIR) diff --git a/config/environments/development.rb b/config/environments/development.rb
@@ -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
(DIR) 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', :as => :all_projects
- 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_job
+ 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_job
match '/projects/:project_id/scans' => 'jobs#results', :as => :results
match '/projects/:project_id/scans/:id' => 'jobs#view_results', :as => :view_results
(DIR) diff --git a/db/migrate/20130106000000_add_indexes.rb b/db/migrate/20130106000000_add_indexes.rb
@@ -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
(DIR) 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 system.
-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_project_id"
+
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_line_id"
+ add_index "line_attributes", ["project_id"], :name => "index_line_attributes_on_project_id"
+
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_signature_id"
+
create_table "signatures", :force => true do |t|
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
(DIR) 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 => true, :busy => false}
+ query = {:id => @conf[:target_ids], :answered => true, :busy => false}
else
- query = {:id => @conf[:target_id], :answered => true, :busy => false, :analysis_started_at => nil}
+ query = {:id => @conf[:target_ids], :answered => true, :busy => false, :analysis_started_at => nil}
end
when 'job'
if @conf[:force]
@@ -89,6 +89,9 @@ class Analysis < Base
else
query = {:answered => true, :busy => false, :analysis_started_at => nil}
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 the DB changes mid-iteration