Swap lightbox, working results pages - warvox - Unnamed repository; edit this file 'description' to name the repository. (DIR) Log (DIR) Files (DIR) Refs (DIR) README --- (DIR) commit 9974997874fab6f83540def599d2031acacbf5f1 (DIR) parent 70379df629033bdd61080eba61894c2e9b745c62 (HTM) Author: HD Moore <hd_moore@rapid7.com> Date: Wed, 2 Jan 2013 02:49:15 -0600 Swap lightbox, working results pages Diffstat: M app/assets/javascripts/application… | 2 +- A app/assets/javascripts/bootstrap-l… | 354 +++++++++++++++++++++++++++++++ D app/assets/javascripts/jquery.ligh… | 472 ------------------------------- M app/assets/stylesheets/application… | 1 + A app/assets/stylesheets/bootstrap-l… | 65 +++++++++++++++++++++++++++++++ M app/controllers/analyze_controller… | 4 ++-- M app/controllers/calls_controller.rb | 59 +------------------------------ M app/controllers/jobs_controller.rb | 98 ++++++++++++++++++------------- M app/models/job.rb | 59 +++++++++++++++++++++++-------- M app/views/analyze/view.html.erb | 33 ++++++++++--------------------- M app/views/calls/index.html.erb | 22 +++++++++++----------- M app/views/jobs/index.html.erb | 2 +- A app/views/jobs/results.html.erb | 64 +++++++++++++++++++++++++++++++ M app/views/layouts/application.html… | 3 ++- A app/views/shared/_lightbox_freq.ht… | 13 +++++++++++++ A app/views/shared/_lightbox_sig.htm… | 13 +++++++++++++ A app/views/shared/lightbox_sig.html… | 17 +++++++++++++++++ M bin/analyze_result.rb | 10 +++++++--- M config/routes.rb | 18 +++++++++--------- M lib/warvox/jobs/analysis.rb | 50 +++++++++++++++++++------------ M lib/warvox/jobs/base.rb | 4 ++++ 21 files changed, 708 insertions(+), 655 deletions(-) --- (DIR) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js @@ -3,7 +3,7 @@ //= require jquery //= require jquery_ujs //= require twitter/bootstrap -//= require jquery.lightbox-0.5 +//= require bootstrap-lightbox //= require dataTables/jquery.dataTables //= require dataTables/jquery.dataTables.bootstrap //= require highcharts (DIR) diff --git a/app/assets/javascripts/bootstrap-lightbox.js b/app/assets/javascripts/bootstrap-lightbox.js @@ -0,0 +1,353 @@ +/*!========================================================= +* bootstrap-lightbox v0.4.1 - 11/20/2012 +* http://jbutz.github.com/bootstrap-lightbox/ +* HEAVILY based off bootstrap-modal.js +* ========================================================== +* Copyright (c) 2012 Jason Butz (http://jasonbutz.info) +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* ========================================================= */ + + +!function ($) { + // browser:true, jquery:true, node:true, laxbreak:true + "use strict"; // jshint ;_; + + +/* LIGHTBOX CLASS DEFINITION + * ========================= */ + + var Lightbox = function (element, options) + { + this.options = options; + this.$element = $(element) + .delegate('[data-dismiss="lightbox"]', 'click.dismiss.lightbox', $.proxy(this.hide, this)); + + this.options.remote && this.$element.find('.lightbox-body').load(this.options.remote); + + this.cloneSize(); + } + + Lightbox.prototype = { + constructor: Lightbox, + + toggle: function () + { + return this[!this.isShown ? 'show' : 'hide'](); + }, + + show: function () + { + var that = this; + var e = $.Event('show') + + this.$element.trigger(e); + + if (this.isShown || e.isDefaultPrevented()) return; + + + this.isShown = true; + + this.escape(); + + this.backdrop(function () + { + var transition = $.support.transition && that.$element.hasClass('fade'); + + if (!that.$element.parent().length) + { + that.$element.appendTo(document.body); //don't move modals dom position + } + + that.$element + .show(); + + if (transition) + { + that.$element[0].offsetWidth; // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false); + + that.enforceFocus(); + + transition ? + that.$element.one($.support.transition.end, function () { that.centerImage(); that.$element.focus().trigger('shown'); }) : + (function(){ that.centerImage(); that.$element.focus().trigger('shown'); })() + + }); + }, + hide: function (e) + { + e && e.preventDefault(); + + var that = this; + + e = $.Event('hide'); + + this.$element.trigger(e); + + if (!this.isShown || e.isDefaultPrevented()) return; + + this.isShown = false; + + this.escape(); + + $(document).off('focusin.lightbox'); + + this.$element + .removeClass('in') + .attr('aria-hidden', true); + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideLightbox(); + }, + enforceFocus: function () + { + var that = this; + $(document).on('focusin.lightbox', function (e) + { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) + { + that.$element.focus(); + } + }); + }, + escape: function () + { + var that = this; + if (this.isShown && this.options.keyboard) + { + this.$element.on('keyup.dismiss.lightbox', function ( e ) + { + e.which == 27 && that.hide(); + }); + } + else if (!this.isShown) + { + this.$element.off('keyup.dismiss.lightbox'); + } + }, + hideWithTransition: function () + { + var that = this; + var timeout = setTimeout(function () + { + that.$element.off($.support.transition.end); + that.hideLightbox(); + }, 500); + + this.$element.one($.support.transition.end, function () + { + clearTimeout(timeout); + that.hideLightbox(); + }); + }, + hideLightbox: function (that) + { + this.$element + .hide() + .trigger('hidden'); + + this.backdrop(); + }, + removeBackdrop: function () + { + this.$backdrop.remove(); + this.$backdrop = null; + }, + backdrop: function (callback) + { + var that = this; + var animate = this.$element.hasClass('fade') ? 'fade' : ''; + + if (this.isShown && this.options.backdrop) + { + var doAnimate = $.support.transition && animate; + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body); + + this.$backdrop.click( + this.options.backdrop == 'static' ? + $.proxy(this.$element[0].focus, this.$element[0]) : + $.proxy(this.hide, this) + ); + + if (doAnimate) this.$backdrop[0].offsetWidth; // force reflow + + this.$backdrop.addClass('in'); + + doAnimate ? + this.$backdrop.one($.support.transition.end, callback) : + callback(); + + } + else if (!this.isShown && this.$backdrop) + { + this.$backdrop.removeClass('in'); + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) : + this.removeBackdrop(); + + } + else if (callback) + { + callback(); + } + }, + centerImage: function() + { + var that = this; + var resizedOffs = 0; + var $img; + + that.h = that.$element.height(); + that.w = that.$element.width(); + + if(that.options.resizeToFit) + { + + resizedOffs = 10; + $img = that.$element.find('.lightbox-content').find('img:first'); + // Save original filesize + if(!$img.data('osizew')) $img.data('osizew', $img.width()); + if(!$img.data('osizeh')) $img.data('osizeh', $img.height()); + + var osizew = $img.data('osizew'); + var osizeh = $img.data('osizeh'); + + // Resize for window dimension < than image + // Reset previous + $img.css('max-width', 'none'); + $img.css('max-height', 'none'); + + + var sOffs = 40; // STYLE ? + if(that.$element.find('.lightbox-header').length > 0) sOffs += 10; + $img.css('max-width', $(window).width() - sOffs); + $img.css('max-height', $(window).height() - sOffs); + + that.w = $img.width(); + that.h = $img.height(); + } + + that.$element.css({ + "position": "fixed", + "left": ( $(window).width() / 2 ) - ( that.w / 2 ), + "top": ( $(window).height() / 2 ) - ( that.h / 2 ) - resizedOffs + }); + that.enforceFocus(); + }, + cloneSize: function() // The cloneSize function is only run once, but it helps keep image jumping down + { + var that = this; + // Clone the element and append it to the body + // this allows us to get an idea for the size of the lightbox + that.$clone = that.$element.filter(':first').clone() + .css( + { + 'position': 'absolute', + 'top' : -2000, + 'display' : 'block', + 'visibility': 'visible', + 'opacity': 100 + }) + .removeClass('fade') + .appendTo('body'); + + that.h = that.$clone.height(); + that.w = that.$clone.width(); + that.$clone.remove(); + + // try and center the element based on the + // height and width retrieved from the clone + that.$element.css({ + "position": "fixed", + "left": ( $(window).width() / 2 ) - ( that.w / 2 ), + "top": ( $(window).height() / 2 ) - ( that.h / 2 ) + }); + } + } + + +/* LIGHTBOX PLUGIN DEFINITION + * ======================= */ + + $.fn.lightbox = function (option) + { + return this.each(function () + { + var $this = $(this); + var data = $this.data('lightbox'); + var options = $.extend({}, $.fn.lightbox.defaults, $this.data(), typeof option == 'object' && option); + if (!data) $this.data('lightbox', (data = new Lightbox(this, options))); + + if (typeof option == 'string') + data[option]() + else if (options.show) + data.show() + }); + }; + + $.fn.lightbox.defaults = { + backdrop: true, + keyboard: true, + show: true, + resizeToFit: true + }; + + $.fn.lightbox.Constructor = Lightbox; + + +/* LIGHTBOX DATA-API + * ================== */ + + $(document).on('click.lightbox.data-api', '[data-toggle="lightbox"]', function (e) + { + var $this = $(this); + var href = $this.attr('href'); + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))); //strip for ie7 + var option = $target.data('lightbox') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data()); + var img = $this.attr('data-image') || false; + var $imgElem; + + e.preventDefault(); + + if(img) + { + $target.data('original-content', $target.find('.lightbox-content').html()); + $target.find('.lightbox-content').html('<img border="0" src="'+img+'" />'); + } + + $target + .lightbox(option) + .one('hide', function () + { + $this.focus() + }) + .one('hidden',function () + { + if( img ) + { + $target.find('.lightbox-content').html( $target.data('original-content') ); + img = undefined; + } + }); + }) + +}(window.jQuery); +\ No newline at end of file (DIR) diff --git a/app/assets/javascripts/jquery.lightbox-0.5.js b/app/assets/javascripts/jquery.lightbox-0.5.js @@ -1,472 +0,0 @@ -/** - * jQuery lightBox plugin - * This jQuery plugin was inspired and based on Lightbox 2 by Lokesh Dhakar (http://www.huddletogether.com/projects/lightbox2/) - * and adapted to me for use like a plugin from jQuery. - * @name jquery-lightbox-0.5.js - * @author Leandro Vieira Pinho - http://leandrovieira.com - * @version 0.5 - * @date April 11, 2008 - * @category jQuery plugin - * @copyright (c) 2008 Leandro Vieira Pinho (leandrovieira.com) - * @license CCAttribution-ShareAlike 2.5 Brazil - http://creativecommons.org/licenses/by-sa/2.5/br/deed.en_US - * @example Visit http://leandrovieira.com/projects/jquery/lightbox/ for more informations about this jQuery plugin - */ - -// Offering a Custom Alias suport - More info: http://docs.jquery.com/Plugins/Authoring#Custom_Alias -(function($) { - /** - * $ is an alias to jQuery object - * - */ - $.fn.lightBox = function(settings) { - // Settings to configure the jQuery lightBox plugin how you like - settings = jQuery.extend({ - // Configuration related to overlay - overlayBgColor: '#000', // (string) Background color to overlay; inform a hexadecimal value like: #RRGGBB. Where RR, GG, and BB are the hexadecimal values for the red, green, and blue values of the color. - overlayOpacity: 0.8, // (integer) Opacity value to overlay; inform: 0.X. Where X are number from 0 to 9 - // Configuration related to navigation - fixedNavigation: false, // (boolean) Boolean that informs if the navigation (next and prev button) will be fixed or not in the interface. - // Configuration related to images - imageLoading: '/assets/lightbox-ico-loading.gif', // (string) Path and the name of the loading icon - imageBtnPrev: '/assets/lightbox-btn-prev.gif', // (string) Path and the name of the prev button image - imageBtnNext: '/assets/lightbox-btn-next.gif', // (string) Path and the name of the next button image - imageBtnClose: '/assets/lightbox-btn-close.gif', // (string) Path and the name of the close btn - imageBlank: '/assets/lightbox-blank.gif', // (string) Path and the name of a blank image (one pixel) - // Configuration related to container image box - containerBorderSize: 10, // (integer) If you adjust the padding in the CSS for the container, #lightbox-container-image-box, you will need to update this value - containerResizeSpeed: 400, // (integer) Specify the resize duration of container image. These number are miliseconds. 400 is default. - // Configuration related to texts in caption. For example: Image 2 of 8. You can alter either "Image" and "of" texts. - txtImage: 'Image', // (string) Specify text "Image" - txtOf: 'of', // (string) Specify text "of" - // Configuration related to keyboard navigation - keyToClose: 'c', // (string) (c = close) Letter to close the jQuery lightBox interface. Beyond this letter, the letter X and the SCAPE key is used to. - keyToPrev: 'p', // (string) (p = previous) Letter to show the previous image - keyToNext: 'n', // (string) (n = next) Letter to show the next image. - // Don't alter these variables in any way - imageArray: [], - activeImage: 0 - },settings); - // Caching the jQuery object with all elements matched - var jQueryMatchedObj = this; // This, in this context, refer to jQuery object - /** - * Initializing the plugin calling the start function - * - * @return boolean false - */ - function _initialize() { - _start(this,jQueryMatchedObj); // This, in this context, refer to object (link) which the user have clicked - return false; // Avoid the browser following the link - } - /** - * Start the jQuery lightBox plugin - * - * @param object objClicked The object (link) whick the user have clicked - * @param object jQueryMatchedObj The jQuery object with all elements matched - */ - function _start(objClicked,jQueryMatchedObj) { - // Hime some elements to avoid conflict with overlay in IE. These elements appear above the overlay. - $('embed, object, select').css({ 'visibility' : 'hidden' }); - // Call the function to create the markup structure; style some elements; assign events in some elements. - _set_interface(); - // Unset total images in imageArray - settings.imageArray.length = 0; - // Unset image active information - settings.activeImage = 0; - // We have an image set? Or just an image? Let's see it. - if ( jQueryMatchedObj.length == 1 ) { - settings.imageArray.push(new Array(objClicked.getAttribute('href'),objClicked.getAttribute('title'))); - } else { - // Add an Array (as many as we have), with href and title atributes, inside the Array that storage the images references - for ( var i = 0; i < jQueryMatchedObj.length; i++ ) { - settings.imageArray.push(new Array(jQueryMatchedObj[i].getAttribute('href'),jQueryMatchedObj[i].getAttribute('title'))); - } - } - while ( settings.imageArray[settings.activeImage][0] != objClicked.getAttribute('href') ) { - settings.activeImage++; - } - // Call the function that prepares image exibition - _set_image_to_view(); - } - /** - * Create the jQuery lightBox plugin interface - * - * The HTML markup will be like that: - <div id="jquery-overlay"></div> - <div id="jquery-lightbox"> - <div id="lightbox-container-image-box"> - <div id="lightbox-container-image"> - <img src="../fotos/XX.jpg" id="lightbox-image"> - <div id="lightbox-nav"> - <a href="#" id="lightbox-nav-btnPrev"></a> - <a href="#" id="lightbox-nav-btnNext"></a> - </div> - <div id="lightbox-loading"> - <a href="#" id="lightbox-loading-link"> - <img src="..//assets/lightbox-ico-loading.gif"> - </a> - </div> - </div> - </div> - <div id="lightbox-container-image-data-box"> - <div id="lightbox-container-image-data"> - <div id="lightbox-image-details"> - <span id="lightbox-image-details-caption"></span> - <span id="lightbox-image-details-currentNumber"></span> - </div> - <div id="lightbox-secNav"> - <a href="#" id="lightbox-secNav-btnClose"> - <img src="..//assets/lightbox-btn-close.gif"> - </a> - </div> - </div> - </div> - </div> - * - */ - function _set_interface() { - // Apply the HTML markup into body tag - $('body').append('<div id="jquery-overlay"></div><div id="jquery-lightbox"><div id="lightbox-container-image-box"><div id="lightbox-container-image"><img id="lightbox-image"><div style="" id="lightbox-nav"><a href="#" id="lightbox-nav-btnPrev"></a><a href="#" id="lightbox-nav-btnNext"></a></div><div id="lightbox-loading"><a href="#" id="lightbox-loading-link"><img src="' + settings.imageLoading + '"></a></div></div></div><div id="lightbox-container-image-data-box"><div id="lightbox-container-image-data"><div id="lightbox-image-details"><span id="lightbox-image-details-caption"></span><span id="lightbox-image-details-currentNumber"></span></div><div id="lightbox-secNav"><a href="#" id="lightbox-secNav-btnClose"><img src="' + settings.imageBtnClose + '"></a></div></div></div></div>'); - // Get page sizes - var arrPageSizes = ___getPageSize(); - // Style overlay and show it - $('#jquery-overlay').css({ - backgroundColor: settings.overlayBgColor, - opacity: settings.overlayOpacity, - width: arrPageSizes[0], - height: arrPageSizes[1] - }).fadeIn(); - // Get page scroll - var arrPageScroll = ___getPageScroll(); - // Calculate top and left offset for the jquery-lightbox div object and show it - $('#jquery-lightbox').css({ - top: arrPageScroll[1] + (arrPageSizes[3] / 10), - left: arrPageScroll[0] - }).show(); - // Assigning click events in elements to close overlay - $('#jquery-overlay,#jquery-lightbox').click(function() { - _finish(); - }); - // Assign the _finish function to lightbox-loading-link and lightbox-secNav-btnClose objects - $('#lightbox-loading-link,#lightbox-secNav-btnClose').click(function() { - _finish(); - return false; - }); - // If window was resized, calculate the new overlay dimensions - $(window).resize(function() { - // Get page sizes - var arrPageSizes = ___getPageSize(); - // Style overlay and show it - $('#jquery-overlay').css({ - width: arrPageSizes[0], - height: arrPageSizes[1] - }); - // Get page scroll - var arrPageScroll = ___getPageScroll(); - // Calculate top and left offset for the jquery-lightbox div object and show it - $('#jquery-lightbox').css({ - top: arrPageScroll[1] + (arrPageSizes[3] / 10), - left: arrPageScroll[0] - }); - }); - } - /** - * Prepares image exibition; doing a image???s preloader to calculate it???s size - * - */ - function _set_image_to_view() { // show the loading - // Show the loading - $('#lightbox-loading').show(); - if ( settings.fixedNavigation ) { - $('#lightbox-image,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide(); - } else { - // Hide some elements - $('#lightbox-image,#lightbox-nav,#lightbox-nav-btnPrev,#lightbox-nav-btnNext,#lightbox-container-image-data-box,#lightbox-image-details-currentNumber').hide(); - } - // Image preload process - var objImagePreloader = new Image(); - objImagePreloader.onload = function() { - $('#lightbox-image').attr('src',settings.imageArray[settings.activeImage][0]); - // Perfomance an effect in the image container resizing it - _resize_container_image_box(objImagePreloader.width,objImagePreloader.height); - // clear onLoad, IE behaves irratically with animated gifs otherwise - objImagePreloader.onload=function(){}; - }; - objImagePreloader.src = settings.imageArray[settings.activeImage][0]; - }; - /** - * Perfomance an effect in the image container resizing it - * - * @param integer intImageWidth The image???s width that will be showed - * @param integer intImageHeight The image???s height that will be showed - */ - function _resize_container_image_box(intImageWidth,intImageHeight) { - // Get current width and height - var intCurrentWidth = $('#lightbox-container-image-box').width(); - var intCurrentHeight = $('#lightbox-container-image-box').height(); - // Get the width and height of the selected image plus the padding - var intWidth = (intImageWidth + (settings.containerBorderSize * 2)); // Plus the image???s width and the left and right padding value - var intHeight = (intImageHeight + (settings.containerBorderSize * 2)); // Plus the image???s height and the left and right padding value - // Diferences - var intDiffW = intCurrentWidth - intWidth; - var intDiffH = intCurrentHeight - intHeight; - // Perfomance the effect - $('#lightbox-container-image-box').animate({ width: intWidth, height: intHeight },settings.containerResizeSpeed,function() { _show_image(); }); - if ( ( intDiffW == 0 ) && ( intDiffH == 0 ) ) { - if ( $.browser.msie ) { - ___pause(250); - } else { - ___pause(100); - } - } - $('#lightbox-container-image-data-box').css({ width: intImageWidth }); - $('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({ height: intImageHeight + (settings.containerBorderSize * 2) }); - }; - /** - * Show the prepared image - * - */ - function _show_image() { - $('#lightbox-loading').hide(); - $('#lightbox-image').fadeIn(function() { - _show_image_data(); - _set_navigation(); - }); - _preload_neighbor_images(); - }; - /** - * Show the image information - * - */ - function _show_image_data() { - $('#lightbox-container-image-data-box').slideDown('fast'); - $('#lightbox-image-details-caption').hide(); - if ( settings.imageArray[settings.activeImage][1] ) { - $('#lightbox-image-details-caption').html(settings.imageArray[settings.activeImage][1]).show(); - } - // If we have a image set, display 'Image X of X' - if ( settings.imageArray.length > 1 ) { - $('#lightbox-image-details-currentNumber').html(settings.txtImage + ' ' + ( settings.activeImage + 1 ) + ' ' + settings.txtOf + ' ' + settings.imageArray.length).show(); - } - } - /** - * Display the button navigations - * - */ - function _set_navigation() { - $('#lightbox-nav').show(); - - // Instead to define this configuration in CSS file, we define here. And it???s need to IE. Just. - $('#lightbox-nav-btnPrev,#lightbox-nav-btnNext').css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' }); - - // Show the prev button, if not the first image in set - if ( settings.activeImage != 0 ) { - if ( settings.fixedNavigation ) { - $('#lightbox-nav-btnPrev').css({ 'background' : 'url(' + settings.imageBtnPrev + ') left 15% no-repeat' }) - .unbind() - .bind('click',function() { - settings.activeImage = settings.activeImage - 1; - _set_image_to_view(); - return false; - }); - } else { - // Show the images button for Next buttons - $('#lightbox-nav-btnPrev').unbind().hover(function() { - $(this).css({ 'background' : 'url(' + settings.imageBtnPrev + ') left 15% no-repeat' }); - },function() { - $(this).css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' }); - }).show().bind('click',function() { - settings.activeImage = settings.activeImage - 1; - _set_image_to_view(); - return false; - }); - } - } - - // Show the next button, if not the last image in set - if ( settings.activeImage != ( settings.imageArray.length -1 ) ) { - if ( settings.fixedNavigation ) { - $('#lightbox-nav-btnNext').css({ 'background' : 'url(' + settings.imageBtnNext + ') right 15% no-repeat' }) - .unbind() - .bind('click',function() { - settings.activeImage = settings.activeImage + 1; - _set_image_to_view(); - return false; - }); - } else { - // Show the images button for Next buttons - $('#lightbox-nav-btnNext').unbind().hover(function() { - $(this).css({ 'background' : 'url(' + settings.imageBtnNext + ') right 15% no-repeat' }); - },function() { - $(this).css({ 'background' : 'transparent url(' + settings.imageBlank + ') no-repeat' }); - }).show().bind('click',function() { - settings.activeImage = settings.activeImage + 1; - _set_image_to_view(); - return false; - }); - } - } - // Enable keyboard navigation - _enable_keyboard_navigation(); - } - /** - * Enable a support to keyboard navigation - * - */ - function _enable_keyboard_navigation() { - $(document).keydown(function(objEvent) { - _keyboard_action(objEvent); - }); - } - /** - * Disable the support to keyboard navigation - * - */ - function _disable_keyboard_navigation() { - $(document).unbind(); - } - /** - * Perform the keyboard actions - * - */ - function _keyboard_action(objEvent) { - // To ie - if ( objEvent == null ) { - keycode = event.keyCode; - escapeKey = 27; - // To Mozilla - } else { - keycode = objEvent.keyCode; - escapeKey = objEvent.DOM_VK_ESCAPE; - } - // Get the key in lower case form - key = String.fromCharCode(keycode).toLowerCase(); - // Verify the keys to close the ligthBox - if ( ( key == settings.keyToClose ) || ( key == 'x' ) || ( keycode == escapeKey ) ) { - _finish(); - } - // Verify the key to show the previous image - if ( ( key == settings.keyToPrev ) || ( keycode == 37 ) ) { - // If we???re not showing the first image, call the previous - if ( settings.activeImage != 0 ) { - settings.activeImage = settings.activeImage - 1; - _set_image_to_view(); - _disable_keyboard_navigation(); - } - } - // Verify the key to show the next image - if ( ( key == settings.keyToNext ) || ( keycode == 39 ) ) { - // If we???re not showing the last image, call the next - if ( settings.activeImage != ( settings.imageArray.length - 1 ) ) { - settings.activeImage = settings.activeImage + 1; - _set_image_to_view(); - _disable_keyboard_navigation(); - } - } - } - /** - * Preload prev and next images being showed - * - */ - function _preload_neighbor_images() { - if ( (settings.imageArray.length -1) > settings.activeImage ) { - objNext = new Image(); - objNext.src = settings.imageArray[settings.activeImage + 1][0]; - } - if ( settings.activeImage > 0 ) { - objPrev = new Image(); - objPrev.src = settings.imageArray[settings.activeImage -1][0]; - } - } - /** - * Remove jQuery lightBox plugin HTML markup - * - */ - function _finish() { - $('#jquery-lightbox').remove(); - $('#jquery-overlay').fadeOut(function() { $('#jquery-overlay').remove(); }); - // Show some elements to avoid conflict with overlay in IE. These elements appear above the overlay. - $('embed, object, select').css({ 'visibility' : 'visible' }); - } - /** - / THIRD FUNCTION - * getPageSize() by quirksmode.com - * - * @return Array Return an array with page width, height and window width, height - */ - function ___getPageSize() { - var xScroll, yScroll; - if (window.innerHeight && window.scrollMaxY) { - xScroll = window.innerWidth + window.scrollMaxX; - yScroll = window.innerHeight + window.scrollMaxY; - } else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac - xScroll = document.body.scrollWidth; - yScroll = document.body.scrollHeight; - } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari - xScroll = document.body.offsetWidth; - yScroll = document.body.offsetHeight; - } - var windowWidth, windowHeight; - if (self.innerHeight) { // all except Explorer - if(document.documentElement.clientWidth){ - windowWidth = document.documentElement.clientWidth; - } else { - windowWidth = self.innerWidth; - } - windowHeight = self.innerHeight; - } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode - windowWidth = document.documentElement.clientWidth; - windowHeight = document.documentElement.clientHeight; - } else if (document.body) { // other Explorers - windowWidth = document.body.clientWidth; - windowHeight = document.body.clientHeight; - } - // for small pages with total height less then height of the viewport - if(yScroll < windowHeight){ - pageHeight = windowHeight; - } else { - pageHeight = yScroll; - } - // for small pages with total width less then width of the viewport - if(xScroll < windowWidth){ - pageWidth = xScroll; - } else { - pageWidth = windowWidth; - } - arrayPageSize = new Array(pageWidth,pageHeight,windowWidth,windowHeight); - return arrayPageSize; - }; - /** - / THIRD FUNCTION - * getPageScroll() by quirksmode.com - * - * @return Array Return an array with x,y page scroll values. - */ - function ___getPageScroll() { - var xScroll, yScroll; - if (self.pageYOffset) { - yScroll = self.pageYOffset; - xScroll = self.pageXOffset; - } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict - yScroll = document.documentElement.scrollTop; - xScroll = document.documentElement.scrollLeft; - } else if (document.body) {// all other Explorers - yScroll = document.body.scrollTop; - xScroll = document.body.scrollLeft; - } - arrayPageScroll = new Array(xScroll,yScroll); - return arrayPageScroll; - }; - /** - * Stop the code execution from a escified time in milisecond - * - */ - function ___pause(ms) { - var date = new Date(); - curDate = null; - do { var curDate = new Date(); } - while ( curDate - date < ms); - }; - // Return the jQuery object for chaining. The unbind method is used to avoid click conflict when the plugin is called more than once - return this.unbind('click').click(_initialize); - }; -})(jQuery); // Call and execute the function immediately passing the jQuery object (DIR) diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss @@ -7,5 +7,6 @@ *= require formtastic *= require formtastic-bootstrap *= require formtastic-overrides + *= require bootstrap-lightbox *= require dataTables/jquery.dataTables.bootstrap */ (DIR) diff --git a/app/assets/stylesheets/bootstrap-lightbox.css b/app/assets/stylesheets/bootstrap-lightbox.css @@ -0,0 +1,65 @@ +/*!========================================================= +* bootstrap-lightbox v0.4.1 - 11/20/2012 +* http://jbutz.github.com/bootstrap-lightbox/ +* HEAVILY based off bootstrap-modal.js +* ========================================================== +* Copyright (c) 2012 Jason Butz (http://jasonbutz.info) +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* ========================================================= */ +.lightbox { + background-color: transparent; + text-align: center; + line-height: 0; + z-index: 1050; + position: relative; + top: 70px; + outline: none; +} +.lightbox .hide { + display: none; +} +.lightbox .in { + display: block; +} +.lightbox-content { + display: inline-block; + padding: 10px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + /* IE6-7 */ + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.lightbox-header .close { + color: white; + margin-right: -16px; + margin-top: -16px; + font-size: 2em; + opacity: .8; + filter: alpha(opacity=80); +} +.lightbox-header .close :hover { + opacity: .4; + filter: alpha(opacity=40); +} (DIR) diff --git a/app/controllers/analyze_controller.rb b/app/controllers/analyze_controller.rb @@ -30,14 +30,14 @@ class AnalyzeController < ApplicationController :page => params[:page], :order => 'number ASC', :per_page => 10, - :conditions => [ 'completed = ? and processed = ? and busy = ? and line_type = ?', true, true, false, @shown ] + :conditions => [ 'answered = ? and analysis_completed_at IS NOT NULL and busy = ? and line_type = ?', true, false, @shown ] ) else @results = Call.where(:job_id => @job_id).paginate( :page => params[:page], :order => 'number ASC', :per_page => 10, - :conditions => [ 'completed = ? and processed = ? and busy = ?', true, true, false ] + :conditions => [ 'answered = ? and analysis_completed_at IS NOT NULL and busy = ?', true, false ] ) end (DIR) diff --git a/app/controllers/calls_controller.rb b/app/controllers/calls_controller.rb @@ -3,11 +3,10 @@ class CallsController < ApplicationController # GET /calls # GET /calls.xml def index - @jobs = Job.where(:status => 'answered').paginate( + @jobs = @project.jobs.where('task = ? AND completed_at IS NOT NULL', 'dialer').paginate( :page => params[:page], :order => 'id DESC', :per_page => 30 - ) respond_to do |format| @@ -16,62 +15,6 @@ class CallsController < ApplicationController end end - # GET /calls/1/reanalyze - def reanalyze - Call.update_all(['processed = ?', false], ['job_id = ?', params[:id]]) - j = Job.find(params[:id]) - j.processed = false - j.save - - redirect_to :action => 'analyze' - end - - # GET /calls/1/process - # GET /calls/1/process.xml - def analyze - @job_id = params[:id] - @job = Job.find(@job_id) - - if(@job.processed) - redirect_to :controller => 'analyze', :action => 'view', :id => @job_id - return - end - - @dial_data_total = Call.count( - :conditions => [ 'job_id = ? and answered = ?', @job_id, true ] - ) - - @dial_data_done = Call.count( - :conditions => [ 'job_id = ? and processed = ?', @job_id, true ] - ) - - ltypes = Call.find( :all, :select => 'DISTINCT line_type', :conditions => ["job_id = ?", @job_id] ).map{|r| r.line_type} - res_types = {} - - ltypes.each do |k| - next if not k - res_types[k.capitalize.to_sym] = Call.count( - :conditions => ['job_id = ? and line_type = ?', @job_id, k] - ) - end - - @lines_by_type = res_types - - @dial_data_todo = Call.where(:job_id => @job_id).paginate( - :page => params[:page], - :order => 'number ASC', - :per_page => 50, - :conditions => [ 'answered = ? and processed = ? and busy = ?', true, false, false ] - ) - - if @dial_data_todo.length > 0 - res = @job.schedule(:analysis) - unless res - flash[:error] = "Unable to launch analysis job" - end - end - end - # GET /calls/1/view # GET /calls/1/view.xml def view (DIR) diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb @@ -15,6 +15,28 @@ class JobsController < ApplicationController end end + def results + + @jobs = @project.jobs.where('task = ? AND completed_at IS NOT NULL', 'dialer').paginate( + :page => params[:page], + :order => 'id DESC', + :per_page => 30 + ) + + respond_to do |format| + format.html # index.html.erb + format.xml { render :xml => @calls } + end + end + + def view_results + @job = Job.find(params[:id]) + @calls = @job.calls.paginate( + :page => params[:page], + :order => 'id DESC', + :per_page => 30 + ) + end def new_dialer @job = Job.new @@ -53,55 +75,51 @@ class JobsController < ApplicationController end end - def stop - @job = Job.find(params[:id]) - @job.stop - flash[:notice] = "Job has been cancelled" - redirect_to :action => 'index' + def reanalyze_job + @job = Job.find(params[:id]) + @new = Job.new({ + :task => 'analysis', :scope => 'job', :target_id => @job.id, :force => true, + :project_id => @project.id, :status => 'submitted' + }) + respond_to do |format| + if @new.schedule + flash[:notice] = 'Analysis job was successfully created.' + format.html { redirect_to jobs_path } + format.xml { render :xml => @job, :status => :created } + else + flash[:notice] = 'Analysis job could not run: ' + @new.errors.inspect + format.html { redirect_to results_path(@project) } + format.xml { render :xml => @job.errors, :status => :unprocessable_entity } + end + end end - def create - - @job = Job.new(params[:job]) - - if(Provider.find_all_by_enabled(true).length == 0) - @job.errors.add(:base, "No providers have been configured or enabled, this job cannot be run") - respond_to do |format| - format.html { render :action => "new" } - format.xml { render :xml => @job.errors, :status => :unprocessable_entity } - end - return - end - - @job.status = 'submitted' - @job.progress = 0 - @job.started_at = nil - @job.completed_at = nil - @job.range = @job_range.gsub(/[^0-9X:,\n]/m, '') - @job.cid_mask = @cid_mask.gsub(/[^0-9X]/m, '') if @job.cid_mask != "SELF" - - if(@job.range_file.to_s != "") - @job.range = @job.range_file.read.gsub(/[^0-9X:,\n]/m, '') - end - + 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' + }) respond_to do |format| - if @job.save - flash[:notice] = 'Job was successfully created.' - - res = @job.schedule(:dialer) - unless res - flash[:error] = "Unable to launch dialer job" - end - - format.html { redirect_to :action => 'index' } - format.xml { render :xml => @job, :status => :created, :location => @job } + if @new.schedule + flash[:notice] = 'Analysis job was successfully created.' + format.html { redirect_to jobs_path } + format.xml { render :xml => @job, :status => :created } else - format.html { render :action => "new" } + flash[:notice] = 'Analysis job could not run: ' + @new.errors.inspect + format.html { redirect_to results_path(@project) } format.xml { render :xml => @job.errors, :status => :unprocessable_entity } end end end + def stop + @job = Job.find(params[:id]) + @job.stop + flash[:notice] = "Job has been cancelled" + redirect_to :action => 'index' + end + def destroy @job = Job.find(params[:id]) @job.destroy (DIR) diff --git a/app/models/job.rb b/app/models/job.rb @@ -23,6 +23,15 @@ 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" + end + if record.scope == "job" and Job.where(:id => record.target_id.to_i, :task => 'dialer').count == 0 + record.errors[:job_id] << "The job_id is not valid" + end + if record.scope == "project" and Job.where(:id => record.target_id.to_i, :task => 'dialer').count == 0 + record.errors[:project_id] << "The project_id is not valid" + end when 'import' else record.errors[:base] << "Invalid task specified" @@ -31,22 +40,12 @@ class Job < ActiveRecord::Base end - has_many :calls - belongs_to :project - validates_with JobValidator - - def stop - self.class.update_all({ :status => 'cancelled'}, { :id => self.id }) - end + # XXX: Purging a single job will be slow, but deleting the project is fast + has_many :calls, :dependent => :destroy - def update_progress(pct) - if pct >= 100 - self.class.update_all({ :progress => pct, :completed_at => Time.now.utc, :status => 'completed' }, { :id => self.id }) - else - self.class.update_all({ :progress => pct }, { :id => self.id }) - end - end + belongs_to :project + attr_accessible :task, :status validates_presence_of :project_id @@ -62,6 +61,30 @@ class Job < ActiveRecord::Base attr_accessible :range, :seconds, :lines, :cid_mask + attr_accessor :scope + attr_accessor :force + attr_accessor :target_id + + attr_accessible :scope, :force, :target_id + + + validates_with JobValidator + + def stop + self.class.update_all({ :status => 'cancelled'}, { :id => self.id }) + end + + def update_progress(pct) + if pct >= 100 + self.class.update_all({ :progress => pct, :completed_at => Time.now.utc, :status => 'completed' }, { :id => self.id }) + else + self.class.update_all({ :progress => pct }, { :id => self.id }) + end + end + + def details + Marshal.load(self.args) rescue {} + end def schedule case task @@ -75,7 +98,13 @@ class Job < ActiveRecord::Base }) return self.save when 'analysis' - # + self.status = 'submitted' + 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 + }) + return self.save else raise ::RuntimeError, "Unsupported Job type" end (DIR) diff --git a/app/views/analyze/view.html.erb b/app/views/analyze/view.html.erb @@ -10,7 +10,7 @@ </tr> </table> -<%= raw(will_paginate @results) %> +<%= will_paginate @results, :renderer => BootstrapPagination::Rails %> <table class='table table-striped table-bordered' width='90%'> <thead> @@ -27,34 +27,27 @@ <object type="application/x-shockwave-flash" - data="/assets/musicplayer.swf?song_url=<%=resource_analyze_path(@job_id, call.id, "mp3")%>" + data="/assets/musicplayer.swf?song_url=<%=resource_analyze_path(call.id, "mp3")%>" width="20" height="17" style="margin-bottom: -5px;" > - <param name="movie" value="/assets/musicplayer.swf?song_url=<%=resource_analyze_path(@job_id, call.id, "mp3")%>"></param> + <param name="movie" value="/assets/musicplayer.swf?song_url=<%=resource_analyze_path(call.id, "mp3")%>"></param> <param name="wmode" value="transparent"></param> </object> <b><%= call.number %></b> <hr width='100%' size='1'/> - CallerID: <%= call.cid%><br/> + CallerID: <%= call.caller_id%><br/> Provider: <%=h call.provider.name %><br/> - Audio: <%=h call.seconds %> Seconds<br/> - Ringer: <%=h call.ringtime %> Seconds<br/> + Audio: <%=h call.audio_length %> Seconds<br/> + Ringer: <%=h call.ring_length %> Seconds<br/> </td> <td align='center'> <b><%=h call.line_type.upcase %></b><br/> - <a href="<%=resource_analyze_path(@job_id, call.id, "big_sig_dots")%>" class="lightbox"><img src="<%=resource_analyze_path(@job_id, call.id, "small_sig")%>" /></a> - <a href="<%=resource_analyze_path(@job_id, call.id, "big_freq")%>" class="lightbox"><img src="<%=resource_analyze_path(@job_id, call.id, "small_freq")%>" /></a><br/> - <% (call.signatures||"").split("\n").each do |s| - sid,mat,name = s.split(':', 3) - str = [mat.to_i * 6.4, 255].min - col = ("%.2x" % (255 - str)) * 3 - %> - <div style="color: #<%= col%>;"><%=h name%> (<%=h sid %>@<%=h mat %>)</div> - <% end %> + <%= render :partial => 'shared/lightbox_sig', :locals => { :call => call } %> + <%= render :partial => 'shared/lightbox_freq', :locals => { :call => call } %> <% if call.fprint and call.fprint.length > 0 %> - <a href="<%=view_matches_path(call.id)%>">View Matches</a> + <a href="<%=view_matches_path(@project, call.id)%>">View Matches</a> <% end %> </td> </tr> @@ -62,10 +55,4 @@ </tbody> </table> -<%= raw(will_paginate @results) %> - -<script type="text/javascript"> -$(function() { - $('a.lightbox').lightBox(); -}); -</script> +<%= will_paginate @results, :renderer => BootstrapPagination::Rails %> (DIR) diff --git a/app/views/calls/index.html.erb b/app/views/calls/index.html.erb @@ -1,7 +1,7 @@ <% if @jobs.length > 0 %> <h1 class='title'>Completed Jobs</h1> -<%= raw(will_paginate @jobs) %> +<%= will_paginate @jobs, :renderer => BootstrapPagination::Rails %> <table class='table table-striped table-bordered' width='90%'> <thead> <tr> @@ -15,22 +15,22 @@ </thead> <tbody> -<% @jobs.sort{|a,b| b.id <=> a.id}.each do |job| %> +<% @jobs.each do |job| %> <tr> - <td><%=h job.id %></td> - <td><%=h job.range %></td> - <td><%=h job.cid_mask %></td> - <td><%=h ( - Call.count(:conditions => ['job_id = ? and processed = ?', job.id, true]).to_s + + <td><%= job.id %></td> + <td><%= job.range %></td> + <td><%= job.cid_mask %></td> + <td><%= ( + job.calls.where("analysis_completed_at IS NOT NULL").count.to_s + "/" + - Call.count(:conditions => ['job_id = ?', job.id]).to_s + job.calls.count.to_s )%></td> - <td><%=h job.started_at.localtime.strftime("%Y-%m-%d %H:%M:%S") %></td> + <td><%= job.started_at.localtime.strftime("%Y-%m-%d %H:%M:%S") %></td> <td> <a class="btn btn-mini" href="<%= view_call_path(@project,job) %>" rel="tooltip" title="View Call Connections" ><i class="icon-bar-chart"></i></a> - <% if(job.analysis_completed_at) %> + <% if job.calls.where("analysis_completed_at IS NOT NULL").count > 0 %> <a class="btn btn-mini" href="<%= analyze_call_path(@project,job) %>" rel="tooltip" title="View Call Analysis"><i class="icon-eye-open"></i></a> <a class="btn btn-mini" href="<%= reanalyze_call_path(@project,job) %>" data-confirm="Reprocess this job?" rel="nofollow tooltip" title="Rerun Call Analysis"><i class="icon-refresh"></i></a> <% else %> @@ -45,7 +45,7 @@ </tbody> </table> -<%= raw(will_paginate @jobs) %> +<%= will_paginate @jobs, :renderer => BootstrapPagination::Rails %> <% else %> (DIR) diff --git a/app/views/jobs/index.html.erb b/app/views/jobs/index.html.erb @@ -1,4 +1,4 @@ -<% if(@submitted_jobs.length > 0) %> +<% if @submitted_jobs.length > 0 %> <h1 class='title'>Submitted Jobs</h1> (DIR) diff --git a/app/views/jobs/results.html.erb b/app/views/jobs/results.html.erb @@ -0,0 +1,64 @@ +<% if @jobs.length > 0 %> +<h1 class='title'>Completed Jobs</h1> + +<%= will_paginate @jobs, :renderer => BootstrapPagination::Rails %> +<table class='table table-striped table-bordered' width='90%'> + <thead> + <tr> + <th>ID</th> + <th>Range</th> + <th>CallerID</th> + <th>Connected</th> + <th>Date</th> + <th>User</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + +<% @jobs.each do |job| + + cnt_dialed = job.calls.count.to_i + cnt_answered = job.calls.where("answered = ? and busy = ?", true, false).count.to_i + pct_answered = 0 + unless cnt_dialed == 0 + pct_answered = ((cnt_answered.to_f / cnt_dialed.to_f) * 100).to_i + end +%> + <tr> + <td><%= job.id %></td> + <td><%= job.details[:range].to_s %></td> + <td><%= job.details[:cid_mask].to_s %></td> + <td><span rel="tooltip" title="<%= pct_answered %>% answered"><%= cnt_answered %> / <%= cnt_dialed %></span></td> + + + <td><%= job.created_at.strftime("%Y-%m-%d %H:%M:%s") %></td> + <td><%= job.created_by %></td> + <td> + <a class="btn btn-mini" href="<%= view_results_path(@project,job) %>" rel="tooltip" title="View Call Connections" ><i class="icon-zoom-in"></i></a> + + <% if job.calls.where("analysis_completed_at IS NOT NULL").count > 0 %> + <a class="btn btn-mini" href="<%= view_analyze_path(@project,job) %>" rel="tooltip" title="View Call Analysis"><i class="icon-eye-open"></i></a> + <a class="btn btn-mini" href="<%= reanalyze_job_path(@project,job) %>" data-confirm="Reprocess this job?" rel="nofollow tooltip" title="Rerun Call Analysis"><i class="icon-refresh"></i></a> + <% else %> + <a class="btn btn-mini" href="<%= analyze_job_path(@project,job) %>" data-confirm="Analyze this job?" rel="nofollow tooltip" title="Run Call Analysis"><i class="icon-cog"></i></a> + <% end %> + + <a class="btn btn-mini" href="<%= job_path(job) %>" data-confirm="Delete all data for this job?" data-method="delete" rel="nofollow tooltip" title="Delete Call Data"><i class="icon-trash"></i></a> + </td> + </tr> + +<% end %> +</tbody> +</table> + +<%= will_paginate @jobs, :renderer => BootstrapPagination::Rails %> + +<% else %> + +<h1 class='title'>No Completed Jobs</h1> +<br/> + +<% end %> + +<a class="btn" href="<%= new_dialer_job_path %>"><i class="icon-plus"></i> Start Job </a> (DIR) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb @@ -23,6 +23,7 @@ <%= javascript_tag do %> $(document).ready(function() { $("a").tooltip(); + $("span").tooltip(); }); <% end %> </head> @@ -39,7 +40,7 @@ h(truncate(@project.name, :length => 20)) + ' <i class="icon-chevron-right icon-white"></i>'), project_path(@project), :class => 'project-title') %> </li> - <%= menu_item "Results", calls_path(@project) %> + <%= menu_item "Results", results_path(@project) %> <%= menu_item "Analysis", analyze_path(@project)%> <% end %> (DIR) diff --git a/app/views/shared/_lightbox_freq.html.erb b/app/views/shared/_lightbox_freq.html.erb @@ -0,0 +1,13 @@ +<% + lid = "call_#{call.id}_freq" +%> +<div id="<%= lid %>" class="lightbox hide fade" tabindex="-1" role="dialog" aria-hidden="true"> + <div class='lightbox-header'> + <button type="button" class="close" data-dismiss="lightbox" aria-hidden="true">×</button> + </div> + <div class='lightbox-content'> + <img src="<%=resource_analyze_path(call.id, "big_freq")%>"> + </div> +</div> + +<a data-toggle="lightbox" href="#<%= lid %>"><img src="<%=resource_analyze_path(call.id, "small_freq")%>" alt="<%= call.number %> frequency graph"/></a> (DIR) diff --git a/app/views/shared/_lightbox_sig.html.erb b/app/views/shared/_lightbox_sig.html.erb @@ -0,0 +1,13 @@ +<% + lid = "call_#{call.id}_sig" +%> +<div id="<%= lid %>" class="lightbox hide fade" tabindex="-1" role="dialog" aria-hidden="true"> + <div class='lightbox-header'> + <button type="button" class="close" data-dismiss="lightbox" aria-hidden="true">×</button> + </div> + <div class='lightbox-content'> + <img src="<%=resource_analyze_path(call.id, "big_sig_dots")%>"> + </div> +</div> + +<a data-toggle="lightbox" href="#<%= lid %>"><img src="<%=resource_analyze_path(call.id, "small_sig")%>" alt="<%= call.number %> signal graph" /></a> (DIR) diff --git a/app/views/shared/lightbox_sig.html.erb b/app/views/shared/lightbox_sig.html.erb @@ -0,0 +1,17 @@ +<% + lid = "call_#{call.id}_sig" +%> +<a href="<%=resource_analyze_path(call.id, "big_sig_dots")%>" class="lightbox"> +<img src="<%=resource_analyze_path(call.id, "small_sig")%>" /> +</a> + +<div id="<%= lid %>" class="lightbox hide fade" tabindex="-1" role="dialog" aria-hidden="true"> + <div class='lightbox-header'> + <button type="button" class="close" data-dismiss="lightbox" aria-hidden="true">×</button> + </div> + <div class='lightbox-content'> + <img src="<%=resource_analyze_path(call.id, "big_sig_dots")%>"> + </div> +</div> + +<a data-toggle="lightbox" href="#<%= lid %>"><img src="<%=resource_analyze_path(call.id, "small_sig")%>" /></a> (DIR) diff --git a/bin/analyze_result.rb b/bin/analyze_result.rb @@ -20,12 +20,16 @@ num = ARGV.shift || exit(0) $0 = "warvox(analyzer): #{inp} #{num}" +begin + $stdout.write( Marshal.dump( - WarVOX::Jobs::CallAnalysis.new( - 0 - ).analyze_call( + WarVOX::Jobs::Analysis.analyze_call( inp, num ) ) ) + +rescue ::Errno::EPIPE + # Hide pipe errors (parent is killed when task was cancelled) +end (DIR) diff --git a/config/routes.rb b/config/routes.rb @@ -18,26 +18,26 @@ Web::Application.routes.draw do match '/jobs/analyzer' => 'jobs#analyzer', :as => :analyzer_job match '/jobs/:id/stop' => 'jobs#stop', :as => :stop_job + match '/projects/:project_id/results' => 'jobs#results', :as => :results + match '/projects/:project_id/results/:id' => 'jobs#view_results', :as => :view_results + match '/projects/:project_id/results/:id/analyze' => 'jobs#analyze_job', :as => :analyze_job + match '/projects/:project_id/results/:id/reanalyze' => 'jobs#reanalyze_job', :as => :reanalyze_job + + - match '/projects/:project_id/calls/' => 'calls#index', :as => :calls - match '/projects/:project_id/calls/:id/view' => 'calls#view', :as => :view_call - match '/projects/:project_id/calls/:id/analyze' => 'calls#analyze', :as => :analyze_call - match '/projects/:project_id/calls/:id/reanalyze' => 'calls#reanalyze', :as => :reanalyze_call - match '/projects/:project_id/calls/:id/purge' => 'calls#purge', :as => :purge_call - delete '/projects/:project_id/calls/:id' => 'calls#destroy' match '/projects/:project_id/analyze' => 'analyze#index', :as => :analyze - match '/projects/:project_id/analyze/:id/resource/:result_id/:type' => 'analyze#resource', :as => :resource_analyze + match '/calls/:result_id/:type' => 'analyze#resource', :as => :resource_analyze match '/projects/:project_id/analyze/:id/view' => 'analyze#view', :as => :view_analyze - match '/projects/:project_id/analyze/:call_id/matches' => 'analyze#view_matches', :as => :view_matches - match '/projects/:project_id/analyze/:id/show' => 'analyze#show', :as => :show_analyze + match '/projects/:project_id/analyze/:call_id/matches' => 'analyze#view_matches', :as => :view_matches resources :settings resources :providers resources :users resources :projects resources :jobs + resources :calls match '/about' => 'home#about', :as => :about match '/help' => 'home#help', :as => :help (DIR) diff --git a/lib/warvox/jobs/analysis.rb b/lib/warvox/jobs/analysis.rb @@ -65,15 +65,15 @@ class Analysis < Base case @conf[:scope] when 'job' if @conf[:force] - query = {:job_id => job.id, :answered => true, :busy => false} + query = {:job_id => @conf[:target_id], :answered => true, :busy => false} else - query = {:job_id => job.id, :answered => true, :busy => false, :analysis_started_at => nil} + query = {:job_id => @conf[:target_id], :answered => true, :busy => false, :analysis_started_at => nil} end when 'project' if @conf[:force] - query = {:project_id => job.project_id, :answered => true, :busy => false} + query = {:project_id => @conf[:target_id], :answered => true, :busy => false} else - query = {:project_id => job.project_id, :answered => true, :busy => false, :analysis_started_at => nil} + query = {:project_id => @conf[:target_id], :answered => true, :busy => false, :analysis_started_at => nil} end when 'global' if @conf[:force] @@ -89,23 +89,29 @@ class Analysis < Base @total_calls = Call.count(:conditions => query) @completed_calls = 0 + WarVOX::Log.debug("Conditions are #{query.inspect}") + Call.find_each(:conditions => query) do |call| - while @tasks.length < max_threads - call.analysis_started_at = Time.now.utc - call.analysis_job_id = job.id - @tasks << Thread.new(call) { |c| ::ActiveRecord::Base.connection_pool.with_connection { run_analyze_call(c) }} - end - clear_stale_tasks + if @tasks.length < max_threads + WarVOX::Log.debug("Spawning job for Call #{call.inspect}") + @tasks << Thread.new(call.id, job.id) { |c,j| ::ActiveRecord::Base.connection_pool.with_connection { run_analyze_call(c,j) }} + else + clear_stale_tasks - # Update progress every 10 seconds or so - if Time.now.to_f - last_update.to_f > 10 - update_progress((@completed_calls / @total_calls.to_f) * 100) - last_update = Time.now - end + # Update progress every 10 seconds or so + if Time.now.to_f - last_update.to_f > 10 + update_progress((@completed_calls / @total_calls.to_f) * 100) + last_update = Time.now + end - clear_zombies() + clear_zombies() + end end + @tasks.map {|t| t.join } + clear_stale_tasks + clear_zombies + } end @@ -121,8 +127,14 @@ class Analysis < Base } end - def run_analyze_call(dr) - $stderr.puts "DEBUG: Processing audio for #{dr.number}..." + def run_analyze_call(cid, jid) + + dr = Call.find(cid) + dr.analysis_started_at = Time.now.utc + dr.analysis_job_id = jid + dr.save + + WarVOX::Log.debug("Worker processing audio for #{dr.number}...") bin = File.join(WarVOX::Base, 'bin', 'analyze_result.rb') tmp = Tempfile.new("Analysis") @@ -163,7 +175,7 @@ class Analysis < Base end # Takes the raw file path as an argument, returns a hash - def analyze_call(input, num=nil) + def self.analyze_call(input, num=nil) return if not input return if not File.exist?(input) (DIR) diff --git a/lib/warvox/jobs/base.rb b/lib/warvox/jobs/base.rb @@ -22,6 +22,10 @@ class Base end def clear_zombies + self.class.clear_zombies + end + + def self.clear_zombies begin # Clear zombies just in case... while(Process.waitpid(-1, Process::WNOHANG))