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 |