Commit 007916789e47816715e047c2e2cfe80627f01778

Authored by Robert Lail
Committed by Bob Lail
1 parent 46c690ad
Exists in master and in 1 other branch production

implement bulk actions for errs: resolve, unresolve, delete

app/controllers/apps_controller.rb
... ... @@ -8,11 +8,12 @@ class AppsController < InheritedResources::Base
8 8 respond_to do |format|
9 9 format.html do
10 10 @all_errs = !!params[:all_errs]
11   -
  11 +
12 12 @errs = resource.errs
13 13 @errs = @errs.unresolved unless @all_errs
14 14 @errs = @errs.in_env(params[:environment]).ordered.paginate(:page => params[:page], :per_page => current_user.per_page)
15   -
  15 +
  16 + @selected_errs = params[:errs] || []
16 17 @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
17 18 end
18 19 format.atom do
... ...
app/controllers/errs_controller.rb
1 1 class ErrsController < ApplicationController
2   -
3   - before_filter :find_app, :except => [:index, :all]
4   - before_filter :find_err, :except => [:index, :all]
5   -
  2 + include ActionView::Helpers::TextHelper
  3 +
  4 + before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several]
  5 + before_filter :find_err, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several]
  6 + before_filter :find_selected_errs, :only => [:destroy_several, :resolve_several, :unresolve_several]
  7 +
  8 +
6 9 def index
7 10 app_scope = current_user.admin? ? App.all : current_user.apps
8 11 @errs = Err.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered
  12 + @selected_errs = params[:errs] || []
9 13 respond_to do |format|
10 14 format.html do
11 15 @errs = @errs.paginate(:page => params[:page], :per_page => current_user.per_page)
... ... @@ -13,12 +17,15 @@ class ErrsController &lt; ApplicationController
13 17 format.atom
14 18 end
15 19 end
16   -
  20 +
  21 +
17 22 def all
18 23 app_scope = current_user.admin? ? App.all : current_user.apps
  24 + @selected_errs = params[:errs] || []
19 25 @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page)
20 26 end
21   -
  27 +
  28 +
22 29 def show
23 30 page = (params[:notice] || @err.notices_count)
24 31 page = 1 if page.to_i.zero?
... ... @@ -26,10 +33,11 @@ class ErrsController &lt; ApplicationController
26 33 @notice = @notices.first
27 34 @comment = Comment.new
28 35 end
29   -
  36 +
  37 +
30 38 def create_issue
31 39 set_tracker_params
32   -
  40 +
33 41 if @app.issue_tracker
34 42 @app.issue_tracker.create_issue @err
35 43 else
... ... @@ -41,26 +49,28 @@ class ErrsController &lt; ApplicationController
41 49 flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later."
42 50 redirect_to app_err_path(@app, @err)
43 51 end
44   -
  52 +
  53 +
45 54 def unlink_issue
46 55 @err.update_attribute :issue_link, nil
47 56 redirect_to app_err_path(@app, @err)
48 57 end
49   -
  58 +
  59 +
50 60 def resolve
51 61 # Deal with bug in mongoid where find is returning an Enumberable obj
52 62 @err = @err.first if @err.respond_to?(:first)
53   -
  63 +
54 64 @err.resolve!
55   -
  65 +
56 66 flash[:success] = 'Great news everyone! The err has been resolved.'
57   -
  67 +
58 68 redirect_to :back
59 69 rescue ActionController::RedirectBackError
60 70 redirect_to app_path(@app)
61 71 end
62   -
63   -
  72 +
  73 +
64 74 def create_comment
65 75 @comment = Comment.new(params[:comment].merge(:user_id => current_user.id))
66 76 if @comment.valid?
... ... @@ -72,7 +82,8 @@ class ErrsController &lt; ApplicationController
72 82 end
73 83 redirect_to app_err_path(@app, @err)
74 84 end
75   -
  85 +
  86 +
76 87 def destroy_comment
77 88 @comment = Comment.find(params[:comment_id])
78 89 if @comment.destroy
... ... @@ -82,27 +93,62 @@ class ErrsController &lt; ApplicationController
82 93 end
83 94 redirect_to app_err_path(@app, @err)
84 95 end
85   -
86   -
87   - protected
88   -
89   - def find_app
90   - @app = App.find(params[:app_id])
91   -
92   - # Mongoid Bug: could not chain: current_user.apps.find_by_id!
93   - # apparently finding by 'watchers.email' and 'id' is broken
94   - raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
95   - end
96   -
97   - def find_err
98   - @err = @app.errs.find(params[:id])
99   - end
100   -
101   - def set_tracker_params
102   - IssueTracker.default_url_options[:host] = request.host
103   - IssueTracker.default_url_options[:port] = request.port
104   - IssueTracker.default_url_options[:protocol] = request.scheme
  96 +
  97 +
  98 + def resolve_several
  99 + @selected_errs.each(&:resolve!)
  100 + flash[:success] = "Great news everyone! #{pluralize(@selected_errs.count, 'err has', 'errs have')} been resolved."
  101 + redirect_to :back
  102 + end
  103 +
  104 +
  105 + def unresolve_several
  106 + @selected_errs.each(&:unresolve!)
  107 + flash[:success] = "#{pluralize(@selected_errs.count, 'err has', 'errs have')} been unresolved."
  108 + redirect_to :back
  109 + end
  110 +
  111 +
  112 + def destroy_several
  113 + @selected_errs.each(&:destroy)
  114 + flash[:notice] = "#{pluralize(@selected_errs.count, 'err has', 'errs have')} been deleted."
  115 + redirect_to :back
  116 + end
  117 +
  118 +
  119 +protected
  120 +
  121 +
  122 + def find_app
  123 + @app = App.find(params[:app_id])
  124 +
  125 + # Mongoid Bug: could not chain: current_user.apps.find_by_id!
  126 + # apparently finding by 'watchers.email' and 'id' is broken
  127 + raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
  128 + end
  129 +
  130 +
  131 + def find_err
  132 + @err = @app.errs.find(params[:id])
  133 + end
  134 +
  135 +
  136 + def set_tracker_params
  137 + IssueTracker.default_url_options[:host] = request.host
  138 + IssueTracker.default_url_options[:port] = request.port
  139 + IssueTracker.default_url_options[:protocol] = request.scheme
  140 + end
  141 +
  142 +
  143 + def find_selected_errs
  144 + err_ids = (params[:errs] || []).compact
  145 + if err_ids.empty?
  146 + flash[:notice] = "You have not selected any errors"
  147 + redirect_to :back
  148 + else
  149 + @selected_errs = Array(Err.find(err_ids))
105 150 end
106   -
  151 + end
  152 +
  153 +
107 154 end
108   -
... ...
app/models/err.rb
... ... @@ -41,6 +41,10 @@ class Err
41 41 self.update_attributes!(:resolved => true)
42 42 end
43 43  
  44 + def unresolve!
  45 + self.update_attributes!(:resolved => false)
  46 + end
  47 +
44 48 def unresolved?
45 49 !resolved?
46 50 end
... ... @@ -56,4 +60,3 @@ class Err
56 60 end
57 61  
58 62 end
59   -
... ...
app/views/errs/_table.html.haml
1   -%table.errs
2   - %thead
3   - %tr
4   - %th App
5   - %th What &amp; Where
6   - %th Latest
7   - %th Deploy
8   - %th Count
9   - %th Resolve
10   - %tbody
11   - - errs.each do |err|
12   - %tr{:class => err.resolved? ? 'resolved' : 'unresolved'}
13   - %td.app
14   - = link_to err.app.name, app_path(err.app)
15   - - if current_page?(:controller => 'errs')
16   - %span.environment= link_to err.environment, errs_path(:environment => err.environment)
17   - - else
18   - %span.environment= link_to err.environment, app_path(err.app, :environment => err.environment)
19   - %td.message
20   - = link_to err.message, app_err_path(err.app, err)
21   - %em= err.where
22   - %td.latest #{time_ago_in_words(last_notice_at err)} ago
23   - %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a'
24   - %td.count= link_to err.notices_count, app_err_path(err.app, err)
25   - %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(err.app, err), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if err.unresolved?
26   - - if errs.none?
  1 +=form_tag do
  2 + %table.errs.selectable
  3 + %thead
27 4 %tr
28   - %td{:colspan => 6}
29   - %em No errs here
30   -= will_paginate @errs, :previous_label => '&laquo; Previous', :next_label => 'Next &raquo;'
  5 + %th
  6 + %th App
  7 + %th What &amp; Where
  8 + %th Latest
  9 + %th Deploy
  10 + %th Count
  11 + %th Resolve
  12 + %tbody
  13 + - errs.each do |err|
  14 + %tr{:class => err.resolved? ? 'resolved' : 'unresolved'}
  15 + %td.select
  16 + = check_box_tag "errs[]", err.id, @selected_errs.member?(err.id.to_s)
  17 + %td.app
  18 + = link_to err.app.name, app_path(err.app)
  19 + - if current_page?(:controller => 'errs')
  20 + %span.environment= link_to err.environment, errs_path(environment: err.environment)
  21 + - else
  22 + %span.environment= link_to err.environment, app_path(err.app, environment: err.environment)
  23 + %td.message
  24 + = link_to err.message, app_err_path(err.app, err)
  25 + %em= err.where
  26 + %td.latest #{time_ago_in_words(last_notice_at err)} ago
  27 + %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a'
  28 + %td.count= link_to err.notices.count, app_err_path(err.app, err)
  29 + %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(err.app, err), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if err.unresolved?
  30 + - if errs.none?
  31 + %tr
  32 + %td{:colspan => (@app ? 5 : 6)}
  33 + %em No errs here
  34 + = will_paginate @errs, :previous_label => '&laquo; Previous', :next_label => 'Next &raquo;'
  35 + .tab-bar
  36 + %ul
  37 + %li= submit_tag 'Resolve', :id => 'resolve_errs', :class => 'button', 'data-action' => resolve_several_errs_path
  38 + %li= submit_tag 'Unresolve', :id => 'unresolve_errs', :class => 'button', 'data-action' => unresolve_several_errs_path
  39 + %li= submit_tag 'Delete', :id => 'delete_errs', :class => 'button', 'data-action' => destroy_several_errs_path
... ...
config/application.rb
... ... @@ -19,43 +19,42 @@ module Errbit
19 19 # Settings in config/environments/* take precedence over those specified here.
20 20 # Application configuration should go into files in config/initializers
21 21 # -- all .rb files in that directory are automatically loaded.
22   -
  22 +
23 23 # Custom directories with classes and modules you want to be autoloadable.
24 24 config.autoload_paths += [Rails.root.join("app/models/issue_trackers")]
25   -
  25 +
26 26 # Only load the plugins named here, in the order given (default is alphabetical).
27 27 # :all can be used as a placeholder for all plugins not explicitly named.
28 28 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
29   -
  29 +
30 30 # Activate observers that should always be running.
31 31 # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
32   -
  32 +
33 33 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
34 34 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
35 35 # config.time_zone = 'Central Time (US & Canada)'
36   -
  36 +
37 37 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
38 38 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
39 39 # config.i18n.default_locale = :de
40   -
  40 +
41 41 # JavaScript files you want as :defaults (application.js is always included).
42   - config.action_view.javascript_expansions[:defaults] = %w(jquery rails form)
43   -
  42 + config.action_view.javascript_expansions[:defaults] = %w(jquery underscore-1.1.6 rails form)
  43 +
44 44 # > rails generate - config
45 45 config.generators do |g|
46 46 g.orm :mongoid
47 47 g.template_engine :haml
48 48 g.test_framework :rspec, :fixture => false
49 49 end
50   -
  50 +
51 51 # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode.
52 52 config.mongoid.preload_models = true
53   -
  53 +
54 54 # Configure the default encoding used in templates for Ruby 1.9.
55 55 config.encoding = "utf-8"
56   -
  56 +
57 57 # Configure sensitive parameters which will be filtered from the log file.
58 58 config.filter_parameters += [:password]
59 59 end
60 60 end
61   -
... ...
config/routes.rb
1 1 Errbit::Application.routes.draw do
2   -
  2 +
3 3 devise_for :users
4   -
  4 +
5 5 # Hoptoad Notifier Routes
6 6 match '/notifier_api/v2/notices' => 'notices#create'
7 7 match '/deploys.txt' => 'deploys#create'
8   -
9   - resources :notices, :only => [:show]
10   - resources :deploys, :only => [:show]
  8 +
  9 + resources :notices, :only => [:show]
  10 + resources :deploys, :only => [:show]
11 11 resources :users
12   - resources :errs, :only => [:index] do
  12 + resources :errs, :only => [:index] do
13 13 collection do
  14 + post :destroy_several
  15 + post :resolve_several
  16 + post :unresolve_several
14 17 get :all
15 18 end
16 19 end
17   -
  20 +
18 21 resources :apps do
19 22 resources :errs do
20 23 resources :notices
21 24 member do
22 25 put :resolve
  26 + put :unresolve
23 27 post :create_issue
24 28 delete :unlink_issue
25 29 post :create_comment
26 30 delete :destroy_comment
27 31 end
28 32 end
29   -
  33 +
30 34 resources :deploys, :only => [:index]
31 35 end
32   -
  36 +
33 37 devise_for :users
34   -
  38 +
35 39 root :to => 'apps#index'
36   -
  40 +
37 41 end
38   -
... ...
public/javascripts/application.js
1 1 // App JS
2 2  
3   -$(function(){
4   - activateTabbedPanels();
5   -
6   - $('#watcher_name').live("click", function() {
7   - $(this).closest('form').find('.show').removeClass('show');
8   - $('#app_watchers_attributes_0_user_id').addClass('show');
9   - });
10   -
11   - $('#watcher_email').live("click", function() {
12   - $(this).closest('form').find('.show').removeClass('show');
13   - $('#app_watchers_attributes_0_email').addClass('show');
14   - });
15   -
16   - $('a.copy_config').live("click", function() {
17   - $('select.choose_other_app').show().focus();
18   - });
19   - $('select.choose_other_app').live("change", function() {
20   - var loc = window.location;
21   - window.location.href = loc.protocol + "//" + loc.host + loc.pathname +
22   - "?copy_attributes_from=" + $(this).val();
23   - });
24   -});
25   -
26   -function activateTabbedPanels() {
27   - $('.tab-bar a').each(function(){
28   - var tab = $(this);
  3 +$(function() {
  4 +
  5 + function init() {
  6 +
  7 + activateTabbedPanels();
  8 +
  9 + activateSelectableRows();
  10 +
  11 + $('#watcher_name').live("click", function() {
  12 + $(this).closest('form').find('.show').removeClass('show');
  13 + $('#app_watchers_attributes_0_user_id').addClass('show');
  14 + });
  15 +
  16 + $('#watcher_email').live("click", function() {
  17 + $(this).closest('form').find('.show').removeClass('show');
  18 + $('#app_watchers_attributes_0_email').addClass('show');
  19 + });
  20 +
  21 + $('a.copy_config').live("click", function() {
  22 + $('select.choose_other_app').show().focus();
  23 + });
  24 +
  25 + $('select.choose_other_app').live("change", function() {
  26 + var loc = window.location;
  27 + window.location.href = loc.protocol + "//" + loc.host + loc.pathname +
  28 + "?copy_attributes_from=" + $(this).val();
  29 + });
  30 +
  31 + $('input[type=submit][data-action]').click(function() {
  32 + $(this).closest('form').attr('action', $(this).attr('data-action'));
  33 + });
  34 + }
  35 +
  36 + function activateTabbedPanels() {
  37 + $('.tab-bar a').each(function(){
  38 + var tab = $(this);
  39 + var panel = $('#'+tab.attr('rel'));
  40 + panel.addClass('panel');
  41 + panel.find('h3').hide();
  42 + })
  43 +
  44 + $('.tab-bar a').click(function(){
  45 + activateTab($(this));
  46 + return(false);
  47 + });
  48 + activateTab($('.tab-bar a').first());
  49 + }
  50 +
  51 + function activateTab(tab) {
  52 + tab = $(tab);
29 53 var panel = $('#'+tab.attr('rel'));
30   - panel.addClass('panel');
31   - panel.find('h3').hide();
32   - })
33   -
34   - $('.tab-bar a').click(function(){
35   - activateTab($(this));
36   - return(false);
37   - });
38   - activateTab($('.tab-bar a').first());
39   -}
40   -
41   -function activateTab(tab) {
42   - tab = $(tab);
43   - var panel = $('#'+tab.attr('rel'));
44   -
45   - tab.closest('.tab-bar').find('a.active').removeClass('active');
46   - tab.addClass('active');
47   -
48   - $('.panel').hide();
49   - panel.show();
50   -}
51   -
  54 +
  55 + tab.closest('.tab-bar').find('a.active').removeClass('active');
  56 + tab.addClass('active');
  57 +
  58 + $('.panel').hide();
  59 + panel.show();
  60 + }
  61 +
  62 + function activateSelectableRows() {
  63 + $('.selectable tr').click(function(event) {
  64 + if(!_.include(['A', 'INPUT', 'BUTTON', 'TEXTAREA'], event.target.nodeName)) {
  65 + var checkbox = $(this).find('input[name="errs[]"]');
  66 + checkbox.attr('checked', !checkbox.is(':checked'));
  67 + }
  68 + })
  69 + }
  70 +
  71 + init();
  72 +});
... ...
public/javascripts/underscore-1.1.6.js 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +// Underscore.js 1.1.6
  2 +// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
  3 +// Underscore is freely distributable under the MIT license.
  4 +// Portions of Underscore are inspired or borrowed from Prototype,
  5 +// Oliver Steele's Functional, and John Resig's Micro-Templating.
  6 +// For all details and documentation:
  7 +// http://documentcloud.github.com/underscore
  8 +(function(){var p=this,C=p._,m={},i=Array.prototype,n=Object.prototype,f=i.slice,D=i.unshift,E=n.toString,l=n.hasOwnProperty,s=i.forEach,t=i.map,u=i.reduce,v=i.reduceRight,w=i.filter,x=i.every,y=i.some,o=i.indexOf,z=i.lastIndexOf;n=Array.isArray;var F=Object.keys,q=Function.prototype.bind,b=function(a){return new j(a)};typeof module!=="undefined"&&module.exports?(module.exports=b,b._=b):p._=b;b.VERSION="1.1.6";var h=b.each=b.forEach=function(a,c,d){if(a!=null)if(s&&a.forEach===s)a.forEach(c,d);else if(b.isNumber(a.length))for(var e=
  9 +0,k=a.length;e<k;e++){if(c.call(d,a[e],e,a)===m)break}else for(e in a)if(l.call(a,e)&&c.call(d,a[e],e,a)===m)break};b.map=function(a,c,b){var e=[];if(a==null)return e;if(t&&a.map===t)return a.map(c,b);h(a,function(a,g,G){e[e.length]=c.call(b,a,g,G)});return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var k=d!==void 0;a==null&&(a=[]);if(u&&a.reduce===u)return e&&(c=b.bind(c,e)),k?a.reduce(c,d):a.reduce(c);h(a,function(a,b,f){!k&&b===0?(d=a,k=!0):d=c.call(e,d,a,b,f)});if(!k)throw new TypeError("Reduce of empty array with no initial value");
  10 +return d};b.reduceRight=b.foldr=function(a,c,d,e){a==null&&(a=[]);if(v&&a.reduceRight===v)return e&&(c=b.bind(c,e)),d!==void 0?a.reduceRight(c,d):a.reduceRight(c);a=(b.isArray(a)?a.slice():b.toArray(a)).reverse();return b.reduce(a,c,d,e)};b.find=b.detect=function(a,c,b){var e;A(a,function(a,g,f){if(c.call(b,a,g,f))return e=a,!0});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(w&&a.filter===w)return a.filter(c,b);h(a,function(a,g,f){c.call(b,a,g,f)&&(e[e.length]=a)});return e};
  11 +b.reject=function(a,c,b){var e=[];if(a==null)return e;h(a,function(a,g,f){c.call(b,a,g,f)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=!0;if(a==null)return e;if(x&&a.every===x)return a.every(c,b);h(a,function(a,g,f){if(!(e=e&&c.call(b,a,g,f)))return m});return e};var A=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=!1;if(a==null)return e;if(y&&a.some===y)return a.some(c,d);h(a,function(a,b,f){if(e=c.call(d,a,b,f))return m});return e};b.include=b.contains=function(a,c){var b=
  12 +!1;if(a==null)return b;if(o&&a.indexOf===o)return a.indexOf(c)!=-1;A(a,function(a){if(b=a===c)return!0});return b};b.invoke=function(a,c){var d=f.call(arguments,2);return b.map(a,function(a){return(c.call?c||a:a[c]).apply(a,d)})};b.pluck=function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,
  13 +c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b<e.computed&&(e={value:a,computed:b})});return e.value};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,f){return{value:a,criteria:c.call(d,a,b,f)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),"value")};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=
  14 +function(a){if(!a)return[];if(a.toArray)return a.toArray();if(b.isArray(a))return a;if(b.isArguments(a))return f.call(a);return b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?f.call(a,0,b):a[0]};b.rest=b.tail=function(a,b,d){return f.call(a,b==null||d?1:b)};b.last=function(a){return a[a.length-1]};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a){return b.reduce(a,function(a,d){if(b.isArray(d))return a.concat(b.flatten(d));
  15 +a[a.length]=d;return a},[])};b.without=function(a){var c=f.call(arguments,1);return b.filter(a,function(a){return!b.include(c,a)})};b.uniq=b.unique=function(a,c){return b.reduce(a,function(a,e,f){if(0==f||(c===!0?b.last(a)!=e:!b.include(a,e)))a[a.length]=e;return a},[])};b.intersect=function(a){var c=f.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.zip=function(){for(var a=f.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),
  16 +e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(o&&a.indexOf===o)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);d=arguments[2]||1;for(var e=Math.max(Math.ceil((b-a)/
  17 +d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};b.bind=function(a,b){if(a.bind===q&&q)return q.apply(a,f.call(arguments,1));var d=f.call(arguments,2);return function(){return a.apply(b,d.concat(f.call(arguments)))}};b.bindAll=function(a){var c=f.call(arguments,1);c.length==0&&(c=b.functions(a));h(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var b=c.apply(this,arguments);return l.call(d,b)?d[b]:d[b]=a.apply(this,arguments)}};b.delay=
  18 +function(a,b){var d=f.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(f.call(arguments,1)))};var B=function(a,b,d){var e;return function(){var f=this,g=arguments,h=function(){e=null;a.apply(f,g)};d&&clearTimeout(e);if(d||!e)e=setTimeout(h,b)}};b.throttle=function(a,b){return B(a,b,!1)};b.debounce=function(a,b){return B(a,b,!0)};b.once=function(a){var b=!1,d;return function(){if(b)return d;b=!0;return d=a.apply(this,arguments)}};
  19 +b.wrap=function(a,b){return function(){var d=[a].concat(f.call(arguments));return b.apply(this,d)}};b.compose=function(){var a=f.call(arguments);return function(){for(var b=f.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=F||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,
  20 +b.identity)};b.functions=b.methods=function(a){return b.filter(b.keys(a),function(c){return b.isFunction(a[c])}).sort()};b.extend=function(a){h(f.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){h(f.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,c){if(a===c)return!0;var d=typeof a;if(d!=
  21 +typeof c)return!1;if(a==c)return!0;if(!a&&c||a&&!c)return!1;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual)return a.isEqual(c);if(b.isDate(a)&&b.isDate(c))return a.getTime()===c.getTime();if(b.isNaN(a)&&b.isNaN(c))return!1;if(b.isRegExp(a)&&b.isRegExp(c))return a.source===c.source&&a.global===c.global&&a.ignoreCase===c.ignoreCase&&a.multiline===c.multiline;if(d!=="object")return!1;if(a.length&&a.length!==c.length)return!1;d=b.keys(a);var e=b.keys(c);if(d.length!=e.length)return!1;
  22 +for(var f in a)if(!(f in c)||!b.isEqual(a[f],c[f]))return!1;return!0};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return!1;return!0};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=n||function(a){return E.call(a)==="[object Array]"};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};
  23 +b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===!0||a===!1};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===!1))};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){p._=C;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=
  24 +0;e<a;e++)b.call(d,e)};b.mixin=function(a){h(b.functions(a),function(c){H(c,b[c]=a[c])})};var I=0;b.uniqueId=function(a){var b=I++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||
  25 +null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return c?d(c):d};var j=function(a){this._wrapped=a};b.prototype=j.prototype;var r=function(a,c){return c?b(a).chain():a},H=function(a,c){j.prototype[a]=function(){var a=f.call(arguments);D.call(a,this._wrapped);return r(c.apply(b,a),this._chain)}};b.mixin(b);h(["pop","push","reverse","shift","sort",
  26 +"splice","unshift"],function(a){var b=i[a];j.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});h(["concat","join","slice"],function(a){var b=i[a];j.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});j.prototype.chain=function(){this._chain=!0;return this};j.prototype.value=function(){return this._wrapped}})();
0 27 \ No newline at end of file
... ...
public/stylesheets/application.css
... ... @@ -244,7 +244,10 @@ a.action { float: right; font-size: 0.9em;}
244 244 }
245 245  
246 246 /* Forms */
247   -form {
  247 +form#new_user,
  248 +form.edit_user,
  249 +form#new_app,
  250 +form.edit_app {
248 251 width: 620px;
249 252 }
250 253 form > div, form fieldset > div { margin: 1em 0;}
... ... @@ -289,7 +292,11 @@ form input[type=submit] {
289 292 font-size: 1.2em; line-height: 1em; text-transform: uppercase;
290 293 border: none; color: #FFF; background-color: #387fc1;
291 294 }
292   -form div.buttons {
  295 +form input[type=submit].button {
  296 + font-size: 1em;
  297 + text-transform: none;
  298 +}
  299 +form div.buttons {
293 300 color: #666;
294 301 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
295 302 border-radius: 50px;
... ... @@ -477,6 +484,7 @@ pre {
477 484 }
478 485  
479 486 /* Buttons */
  487 +input[type="submit"].button,
480 488 a.button {
481 489 display: inline-block;
482 490 padding: 0 0.8em;
... ... @@ -492,6 +500,7 @@ a.button {
492 500 -webkit-box-shadow: inset 0px 0px 4px #FFF;
493 501 line-height: 30px;
494 502 }
  503 +input[type="submit"]:hover.button,
495 504 a:hover.button {
496 505 box-shadow: 0px 0px 4px #ccc;
497 506 -moz-box-shadow: 0px 0px 4px #ccc;
... ... @@ -508,6 +517,7 @@ a.button.active {
508 517 -webkit-box-shadow: inset 0 0 5px #999;
509 518 }
510 519  
  520 +
511 521 /* Tab Bar */
512 522 .tab-bar {
513 523 margin-bottom: 24px;
... ...
spec/controllers/errs_controller_spec.rb
... ... @@ -410,5 +410,39 @@ describe ErrsController do
410 410 end
411 411 end
412 412  
  413 + describe "Bulk Actions" do
  414 + before(:each) do
  415 + sign_in Factory(:admin)
  416 + @err1 = Factory(:err, :resolved => true)
  417 + @err2 = Factory(:err, :resolved => false)
  418 + end
  419 +
  420 + it "should apply to multiple errs" do
  421 + post :resolve_several, :errs => [@err1.id.to_s, @err2.id.to_s]
  422 + assigns(:selected_errs).should == [@err1, @err2]
  423 + end
  424 +
  425 + context "POST /errs/resolve_several" do
  426 + it "should resolve the issue" do
  427 + post :resolve_several, :errs => [@err2.id.to_s]
  428 + @err2.reload.resolved?.should == true
  429 + end
  430 + end
  431 +
  432 + context "POST /errs/unresolve_several" do
  433 + it "should unresolve the issue" do
  434 + post :unresolve_several, :errs => [@err1.id.to_s]
  435 + @err1.reload.resolved?.should == false
  436 + end
  437 + end
  438 +
  439 + context "POST /errs/destroy_several" do
  440 + it "should delete the errs" do
  441 + lambda {
  442 + post :destroy_several, :errs => [@err1.id.to_s]
  443 + }.should change(Err, :count).by(-1)
  444 + end
  445 + end
  446 + end
413 447 end
414 448  
... ...