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,11 +8,12 @@ class AppsController < InheritedResources::Base | ||
8 | respond_to do |format| | 8 | respond_to do |format| |
9 | format.html do | 9 | format.html do |
10 | @all_errs = !!params[:all_errs] | 10 | @all_errs = !!params[:all_errs] |
11 | - | 11 | + |
12 | @errs = resource.errs | 12 | @errs = resource.errs |
13 | @errs = @errs.unresolved unless @all_errs | 13 | @errs = @errs.unresolved unless @all_errs |
14 | @errs = @errs.in_env(params[:environment]).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) | 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 | @deploys = @app.deploys.order_by(:created_at.desc).limit(5) | 17 | @deploys = @app.deploys.order_by(:created_at.desc).limit(5) |
17 | end | 18 | end |
18 | format.atom do | 19 | format.atom do |
app/controllers/errs_controller.rb
1 | class ErrsController < ApplicationController | 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 | def index | 9 | def index |
7 | app_scope = current_user.admin? ? App.all : current_user.apps | 10 | app_scope = current_user.admin? ? App.all : current_user.apps |
8 | @errs = Err.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered | 11 | @errs = Err.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered |
12 | + @selected_errs = params[:errs] || [] | ||
9 | respond_to do |format| | 13 | respond_to do |format| |
10 | format.html do | 14 | format.html do |
11 | @errs = @errs.paginate(:page => params[:page], :per_page => current_user.per_page) | 15 | @errs = @errs.paginate(:page => params[:page], :per_page => current_user.per_page) |
@@ -13,12 +17,15 @@ class ErrsController < ApplicationController | @@ -13,12 +17,15 @@ class ErrsController < ApplicationController | ||
13 | format.atom | 17 | format.atom |
14 | end | 18 | end |
15 | end | 19 | end |
16 | - | 20 | + |
21 | + | ||
17 | def all | 22 | def all |
18 | app_scope = current_user.admin? ? App.all : current_user.apps | 23 | app_scope = current_user.admin? ? App.all : current_user.apps |
24 | + @selected_errs = params[:errs] || [] | ||
19 | @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) | 25 | @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) |
20 | end | 26 | end |
21 | - | 27 | + |
28 | + | ||
22 | def show | 29 | def show |
23 | page = (params[:notice] || @err.notices_count) | 30 | page = (params[:notice] || @err.notices_count) |
24 | page = 1 if page.to_i.zero? | 31 | page = 1 if page.to_i.zero? |
@@ -26,10 +33,11 @@ class ErrsController < ApplicationController | @@ -26,10 +33,11 @@ class ErrsController < ApplicationController | ||
26 | @notice = @notices.first | 33 | @notice = @notices.first |
27 | @comment = Comment.new | 34 | @comment = Comment.new |
28 | end | 35 | end |
29 | - | 36 | + |
37 | + | ||
30 | def create_issue | 38 | def create_issue |
31 | set_tracker_params | 39 | set_tracker_params |
32 | - | 40 | + |
33 | if @app.issue_tracker | 41 | if @app.issue_tracker |
34 | @app.issue_tracker.create_issue @err | 42 | @app.issue_tracker.create_issue @err |
35 | else | 43 | else |
@@ -41,26 +49,28 @@ class ErrsController < ApplicationController | @@ -41,26 +49,28 @@ class ErrsController < ApplicationController | ||
41 | flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later." | 49 | flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later." |
42 | redirect_to app_err_path(@app, @err) | 50 | redirect_to app_err_path(@app, @err) |
43 | end | 51 | end |
44 | - | 52 | + |
53 | + | ||
45 | def unlink_issue | 54 | def unlink_issue |
46 | @err.update_attribute :issue_link, nil | 55 | @err.update_attribute :issue_link, nil |
47 | redirect_to app_err_path(@app, @err) | 56 | redirect_to app_err_path(@app, @err) |
48 | end | 57 | end |
49 | - | 58 | + |
59 | + | ||
50 | def resolve | 60 | def resolve |
51 | # Deal with bug in mongoid where find is returning an Enumberable obj | 61 | # Deal with bug in mongoid where find is returning an Enumberable obj |
52 | @err = @err.first if @err.respond_to?(:first) | 62 | @err = @err.first if @err.respond_to?(:first) |
53 | - | 63 | + |
54 | @err.resolve! | 64 | @err.resolve! |
55 | - | 65 | + |
56 | flash[:success] = 'Great news everyone! The err has been resolved.' | 66 | flash[:success] = 'Great news everyone! The err has been resolved.' |
57 | - | 67 | + |
58 | redirect_to :back | 68 | redirect_to :back |
59 | rescue ActionController::RedirectBackError | 69 | rescue ActionController::RedirectBackError |
60 | redirect_to app_path(@app) | 70 | redirect_to app_path(@app) |
61 | end | 71 | end |
62 | - | ||
63 | - | 72 | + |
73 | + | ||
64 | def create_comment | 74 | def create_comment |
65 | @comment = Comment.new(params[:comment].merge(:user_id => current_user.id)) | 75 | @comment = Comment.new(params[:comment].merge(:user_id => current_user.id)) |
66 | if @comment.valid? | 76 | if @comment.valid? |
@@ -72,7 +82,8 @@ class ErrsController < ApplicationController | @@ -72,7 +82,8 @@ class ErrsController < ApplicationController | ||
72 | end | 82 | end |
73 | redirect_to app_err_path(@app, @err) | 83 | redirect_to app_err_path(@app, @err) |
74 | end | 84 | end |
75 | - | 85 | + |
86 | + | ||
76 | def destroy_comment | 87 | def destroy_comment |
77 | @comment = Comment.find(params[:comment_id]) | 88 | @comment = Comment.find(params[:comment_id]) |
78 | if @comment.destroy | 89 | if @comment.destroy |
@@ -82,27 +93,62 @@ class ErrsController < ApplicationController | @@ -82,27 +93,62 @@ class ErrsController < ApplicationController | ||
82 | end | 93 | end |
83 | redirect_to app_err_path(@app, @err) | 94 | redirect_to app_err_path(@app, @err) |
84 | end | 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 | end | 150 | end |
106 | - | 151 | + end |
152 | + | ||
153 | + | ||
107 | end | 154 | end |
108 | - |
app/models/err.rb
@@ -41,6 +41,10 @@ class Err | @@ -41,6 +41,10 @@ class Err | ||
41 | self.update_attributes!(:resolved => true) | 41 | self.update_attributes!(:resolved => true) |
42 | end | 42 | end |
43 | 43 | ||
44 | + def unresolve! | ||
45 | + self.update_attributes!(:resolved => false) | ||
46 | + end | ||
47 | + | ||
44 | def unresolved? | 48 | def unresolved? |
45 | !resolved? | 49 | !resolved? |
46 | end | 50 | end |
@@ -56,4 +60,3 @@ class Err | @@ -56,4 +60,3 @@ class Err | ||
56 | end | 60 | end |
57 | 61 | ||
58 | end | 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 | %tr | 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,43 +19,42 @@ module Errbit | ||
19 | # Settings in config/environments/* take precedence over those specified here. | 19 | # Settings in config/environments/* take precedence over those specified here. |
20 | # Application configuration should go into files in config/initializers | 20 | # Application configuration should go into files in config/initializers |
21 | # -- all .rb files in that directory are automatically loaded. | 21 | # -- all .rb files in that directory are automatically loaded. |
22 | - | 22 | + |
23 | # Custom directories with classes and modules you want to be autoloadable. | 23 | # Custom directories with classes and modules you want to be autoloadable. |
24 | config.autoload_paths += [Rails.root.join("app/models/issue_trackers")] | 24 | config.autoload_paths += [Rails.root.join("app/models/issue_trackers")] |
25 | - | 25 | + |
26 | # Only load the plugins named here, in the order given (default is alphabetical). | 26 | # Only load the plugins named here, in the order given (default is alphabetical). |
27 | # :all can be used as a placeholder for all plugins not explicitly named. | 27 | # :all can be used as a placeholder for all plugins not explicitly named. |
28 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] | 28 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] |
29 | - | 29 | + |
30 | # Activate observers that should always be running. | 30 | # Activate observers that should always be running. |
31 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer | 31 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer |
32 | - | 32 | + |
33 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. | 33 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. |
34 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. | 34 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. |
35 | # config.time_zone = 'Central Time (US & Canada)' | 35 | # config.time_zone = 'Central Time (US & Canada)' |
36 | - | 36 | + |
37 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. | 37 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. |
38 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] | 38 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] |
39 | # config.i18n.default_locale = :de | 39 | # config.i18n.default_locale = :de |
40 | - | 40 | + |
41 | # JavaScript files you want as :defaults (application.js is always included). | 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 | # > rails generate - config | 44 | # > rails generate - config |
45 | config.generators do |g| | 45 | config.generators do |g| |
46 | g.orm :mongoid | 46 | g.orm :mongoid |
47 | g.template_engine :haml | 47 | g.template_engine :haml |
48 | g.test_framework :rspec, :fixture => false | 48 | g.test_framework :rspec, :fixture => false |
49 | end | 49 | end |
50 | - | 50 | + |
51 | # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. | 51 | # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. |
52 | config.mongoid.preload_models = true | 52 | config.mongoid.preload_models = true |
53 | - | 53 | + |
54 | # Configure the default encoding used in templates for Ruby 1.9. | 54 | # Configure the default encoding used in templates for Ruby 1.9. |
55 | config.encoding = "utf-8" | 55 | config.encoding = "utf-8" |
56 | - | 56 | + |
57 | # Configure sensitive parameters which will be filtered from the log file. | 57 | # Configure sensitive parameters which will be filtered from the log file. |
58 | config.filter_parameters += [:password] | 58 | config.filter_parameters += [:password] |
59 | end | 59 | end |
60 | end | 60 | end |
61 | - |
config/routes.rb
1 | Errbit::Application.routes.draw do | 1 | Errbit::Application.routes.draw do |
2 | - | 2 | + |
3 | devise_for :users | 3 | devise_for :users |
4 | - | 4 | + |
5 | # Hoptoad Notifier Routes | 5 | # Hoptoad Notifier Routes |
6 | match '/notifier_api/v2/notices' => 'notices#create' | 6 | match '/notifier_api/v2/notices' => 'notices#create' |
7 | match '/deploys.txt' => 'deploys#create' | 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 | resources :users | 11 | resources :users |
12 | - resources :errs, :only => [:index] do | 12 | + resources :errs, :only => [:index] do |
13 | collection do | 13 | collection do |
14 | + post :destroy_several | ||
15 | + post :resolve_several | ||
16 | + post :unresolve_several | ||
14 | get :all | 17 | get :all |
15 | end | 18 | end |
16 | end | 19 | end |
17 | - | 20 | + |
18 | resources :apps do | 21 | resources :apps do |
19 | resources :errs do | 22 | resources :errs do |
20 | resources :notices | 23 | resources :notices |
21 | member do | 24 | member do |
22 | put :resolve | 25 | put :resolve |
26 | + put :unresolve | ||
23 | post :create_issue | 27 | post :create_issue |
24 | delete :unlink_issue | 28 | delete :unlink_issue |
25 | post :create_comment | 29 | post :create_comment |
26 | delete :destroy_comment | 30 | delete :destroy_comment |
27 | end | 31 | end |
28 | end | 32 | end |
29 | - | 33 | + |
30 | resources :deploys, :only => [:index] | 34 | resources :deploys, :only => [:index] |
31 | end | 35 | end |
32 | - | 36 | + |
33 | devise_for :users | 37 | devise_for :users |
34 | - | 38 | + |
35 | root :to => 'apps#index' | 39 | root :to => 'apps#index' |
36 | - | 40 | + |
37 | end | 41 | end |
38 | - |
public/javascripts/application.js
1 | // App JS | 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 | var panel = $('#'+tab.attr('rel')); | 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 @@ | @@ -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 | \ No newline at end of file | 27 | \ No newline at end of file |
public/stylesheets/application.css
@@ -244,7 +244,10 @@ a.action { float: right; font-size: 0.9em;} | @@ -244,7 +244,10 @@ a.action { float: right; font-size: 0.9em;} | ||
244 | } | 244 | } |
245 | 245 | ||
246 | /* Forms */ | 246 | /* Forms */ |
247 | -form { | 247 | +form#new_user, |
248 | +form.edit_user, | ||
249 | +form#new_app, | ||
250 | +form.edit_app { | ||
248 | width: 620px; | 251 | width: 620px; |
249 | } | 252 | } |
250 | form > div, form fieldset > div { margin: 1em 0;} | 253 | form > div, form fieldset > div { margin: 1em 0;} |
@@ -289,7 +292,11 @@ form input[type=submit] { | @@ -289,7 +292,11 @@ form input[type=submit] { | ||
289 | font-size: 1.2em; line-height: 1em; text-transform: uppercase; | 292 | font-size: 1.2em; line-height: 1em; text-transform: uppercase; |
290 | border: none; color: #FFF; background-color: #387fc1; | 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 | color: #666; | 300 | color: #666; |
294 | background: #FFF url(images/button-bg.png) 0 bottom repeat-x; | 301 | background: #FFF url(images/button-bg.png) 0 bottom repeat-x; |
295 | border-radius: 50px; | 302 | border-radius: 50px; |
@@ -477,6 +484,7 @@ pre { | @@ -477,6 +484,7 @@ pre { | ||
477 | } | 484 | } |
478 | 485 | ||
479 | /* Buttons */ | 486 | /* Buttons */ |
487 | +input[type="submit"].button, | ||
480 | a.button { | 488 | a.button { |
481 | display: inline-block; | 489 | display: inline-block; |
482 | padding: 0 0.8em; | 490 | padding: 0 0.8em; |
@@ -492,6 +500,7 @@ a.button { | @@ -492,6 +500,7 @@ a.button { | ||
492 | -webkit-box-shadow: inset 0px 0px 4px #FFF; | 500 | -webkit-box-shadow: inset 0px 0px 4px #FFF; |
493 | line-height: 30px; | 501 | line-height: 30px; |
494 | } | 502 | } |
503 | +input[type="submit"]:hover.button, | ||
495 | a:hover.button { | 504 | a:hover.button { |
496 | box-shadow: 0px 0px 4px #ccc; | 505 | box-shadow: 0px 0px 4px #ccc; |
497 | -moz-box-shadow: 0px 0px 4px #ccc; | 506 | -moz-box-shadow: 0px 0px 4px #ccc; |
@@ -508,6 +517,7 @@ a.button.active { | @@ -508,6 +517,7 @@ a.button.active { | ||
508 | -webkit-box-shadow: inset 0 0 5px #999; | 517 | -webkit-box-shadow: inset 0 0 5px #999; |
509 | } | 518 | } |
510 | 519 | ||
520 | + | ||
511 | /* Tab Bar */ | 521 | /* Tab Bar */ |
512 | .tab-bar { | 522 | .tab-bar { |
513 | margin-bottom: 24px; | 523 | margin-bottom: 24px; |
spec/controllers/errs_controller_spec.rb
@@ -410,5 +410,39 @@ describe ErrsController do | @@ -410,5 +410,39 @@ describe ErrsController do | ||
410 | end | 410 | end |
411 | end | 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 | end | 447 | end |
414 | 448 |