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">&times;</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">&times;</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">&times;</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))