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 | ... | ... |