Commit 007916789e47816715e047c2e2cfe80627f01778
Committed by
Bob Lail
1 parent
46c690ad
Exists in
master
and in
1 other branch
implement bulk actions for errs: resolve, unresolve, delete
Showing
10 changed files
with
296 additions
and
144 deletions
Show diff stats
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 < 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 < 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 < 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 < 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 < 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 & 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 => '« Previous', :next_label => 'Next »' | |
5 | + %th | |
6 | + %th App | |
7 | + %th What & 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 => '« Previous', :next_label => 'Next »' | |
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 | +}); | ... | ... |
... | ... | @@ -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 | ... | ... |