From 7270ce2c3116a920eb2b759e4cee83af0383ba2f Mon Sep 17 00:00:00 2001 From: Jared Pace Date: Mon, 23 Aug 2010 17:13:41 -0400 Subject: [PATCH 001/115] Add a fix for Firefox - form buttons don't respect line-height --- public/stylesheets/application.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 03ef5db6fd..2c538de56e 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -294,6 +294,8 @@ form div.buttons input, form div.buttons button { padding: 0 20px; color: #666; background: none; + display: inline-block; + height: 36px; font-size: 14px; font-weight: bold; line-height: 36px; text-decoration: none; text-shadow: 1px 1px 0px #FFF; -moz-text-shadow: 1px 1px 0px #FFF; From 6384237845c33365ba14be84a9f3aa3e9ce18c57 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Mon, 22 Nov 2010 15:54:00 +0300 Subject: [PATCH 002/115] Added .rvmrc to gitignore. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 95666115a0..fe6d1c47bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ log/*.log tmp/**/* config/config.yml config/deploy.rb -config/mongoid.yml \ No newline at end of file +config/mongoid.yml +.rvmrc From cf313c57b6715a640ed69fadab67e5ab2864f853 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Mon, 22 Nov 2010 16:09:43 +0300 Subject: [PATCH 003/115] Massive dependencies update. --- .gitignore | 1 + Gemfile | 12 ++-- Gemfile.lock | 153 +++++++++++++++++++++----------------------- spec/spec_helper.rb | 3 +- 4 files changed, 81 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index fe6d1c47bf..6137e75503 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ config/config.yml config/deploy.rb config/mongoid.yml .rvmrc +*~ diff --git a/Gemfile b/Gemfile index 61e1d07e76..82b710ee88 100644 --- a/Gemfile +++ b/Gemfile @@ -1,19 +1,19 @@ source 'http://rubygems.org' -gem 'rails', '3.0.0.rc' +gem 'rails', '3.0.3' gem 'libxml-ruby' gem 'bson_ext', :require => nil -gem 'mongoid', '2.0.0.beta.15' +gem 'mongoid', '~> 2.0.0.beta.20' gem 'haml' gem 'will_paginate' -gem 'devise', '1.1.1' +gem 'devise', '~> 1.1.3' group :development, :test do - gem 'rspec-rails', '>= 2.0.0.beta.19' + gem 'rspec-rails', '~> 2.1' end group :test do - gem 'rspec', '>= 2.0.0.beta.19' - gem 'database_cleaner', '0.5.2' + gem 'rspec', '~> 2.1' + gem 'database_cleaner', '~> 0.6.0' gem 'factory_girl_rails' end diff --git a/Gemfile.lock b/Gemfile.lock index 68a23b873b..57d8dab63e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,40 +2,39 @@ GEM remote: http://rubygems.org/ specs: abstract (1.0.0) - actionmailer (3.0.0.rc) - actionpack (= 3.0.0.rc) - mail (~> 2.2.5) - actionpack (3.0.0.rc) - activemodel (= 3.0.0.rc) - activesupport (= 3.0.0.rc) + actionmailer (3.0.3) + actionpack (= 3.0.3) + mail (~> 2.2.9) + actionpack (3.0.3) + activemodel (= 3.0.3) + activesupport (= 3.0.3) builder (~> 2.1.2) erubis (~> 2.6.6) - i18n (~> 0.4.1) + i18n (~> 0.4) rack (~> 1.2.1) - rack-mount (~> 0.6.9) - rack-test (~> 0.5.4) - tzinfo (~> 0.3.22) - activemodel (3.0.0.rc) - activesupport (= 3.0.0.rc) + rack-mount (~> 0.6.13) + rack-test (~> 0.5.6) + tzinfo (~> 0.3.23) + activemodel (3.0.3) + activesupport (= 3.0.3) builder (~> 2.1.2) - i18n (~> 0.4.1) - activerecord (3.0.0.rc) - activemodel (= 3.0.0.rc) - activesupport (= 3.0.0.rc) - arel (~> 0.4.0) - tzinfo (~> 0.3.22) - activeresource (3.0.0.rc) - activemodel (= 3.0.0.rc) - activesupport (= 3.0.0.rc) - activesupport (3.0.0.rc) - arel (0.4.0) - activesupport (>= 3.0.0.beta) + i18n (~> 0.4) + activerecord (3.0.3) + activemodel (= 3.0.3) + activesupport (= 3.0.3) + arel (~> 2.0.2) + tzinfo (~> 0.3.23) + activeresource (3.0.3) + activemodel (= 3.0.3) + activesupport (= 3.0.3) + activesupport (3.0.3) + arel (2.0.4) bcrypt-ruby (2.1.2) - bson (1.0.4) - bson_ext (1.0.4) + bson (1.1.2) + bson_ext (1.1.2) builder (2.1.2) - database_cleaner (0.5.2) - devise (1.1.1) + database_cleaner (0.6.0) + devise (1.1.3) bcrypt-ruby (~> 2.1.2) warden (~> 0.10.7) diff-lcs (1.1.2) @@ -45,64 +44,58 @@ GEM factory_girl_rails (1.0) factory_girl (~> 1.3) rails (>= 3.0.0.beta4) - haml (3.0.16) - i18n (0.4.1) + haml (3.0.24) + i18n (0.4.2) libxml-ruby (1.1.4) - mail (2.2.5) + mail (2.2.10) activesupport (>= 2.3.6) - mime-types - treetop (>= 1.4.5) + i18n (~> 0.4.1) + mime-types (~> 1.16) + treetop (~> 1.4.8) mime-types (1.16) - mongo (1.0.6) - bson (>= 1.0.4) - mongoid (2.0.0.beta.15) - activemodel (= 3.0.0.rc) - bson (= 1.0.4) - mongo (= 1.0.6) - tzinfo (= 0.3.22) + mongo (1.1.2) + bson (>= 1.1.1) + mongoid (2.0.0.beta.20) + activemodel (~> 3.0) + mongo (~> 1.1) + tzinfo (~> 0.3.22) will_paginate (~> 3.0.pre) - nokogiri (1.4.3.1) polyglot (0.3.1) rack (1.2.1) - rack-mount (0.6.9) + rack-mount (0.6.13) rack (>= 1.0.0) - rack-test (0.5.4) + rack-test (0.5.6) rack (>= 1.0) - rails (3.0.0.rc) - actionmailer (= 3.0.0.rc) - actionpack (= 3.0.0.rc) - activerecord (= 3.0.0.rc) - activeresource (= 3.0.0.rc) - activesupport (= 3.0.0.rc) - bundler (>= 1.0.0.rc.1) - railties (= 3.0.0.rc) - railties (3.0.0.rc) - actionpack (= 3.0.0.rc) - activesupport (= 3.0.0.rc) - rake (>= 0.8.3) - thor (~> 0.14.0) + rails (3.0.3) + actionmailer (= 3.0.3) + actionpack (= 3.0.3) + activerecord (= 3.0.3) + activeresource (= 3.0.3) + activesupport (= 3.0.3) + bundler (~> 1.0) + railties (= 3.0.3) + railties (3.0.3) + actionpack (= 3.0.3) + activesupport (= 3.0.3) + rake (>= 0.8.7) + thor (~> 0.14.4) rake (0.8.7) - rspec (2.0.0.beta.19) - rspec-core (= 2.0.0.beta.19) - rspec-expectations (= 2.0.0.beta.19) - rspec-mocks (= 2.0.0.beta.19) - rspec-core (2.0.0.beta.19) - rspec-expectations (2.0.0.beta.19) - diff-lcs (>= 1.1.2) - rspec-mocks (2.0.0.beta.19) - rspec-rails (2.0.0.beta.19) - rspec (= 2.0.0.beta.19) - webrat (>= 0.7.2.beta.1) - thor (0.14.0) - treetop (1.4.8) + rspec (2.1.0) + rspec-core (~> 2.1.0) + rspec-expectations (~> 2.1.0) + rspec-mocks (~> 2.1.0) + rspec-core (2.1.0) + rspec-expectations (2.1.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.1.0) + rspec-rails (2.1.0) + rspec (~> 2.1.0) + thor (0.14.6) + treetop (1.4.9) polyglot (>= 0.3.1) - tzinfo (0.3.22) + tzinfo (0.3.23) warden (0.10.7) rack (>= 1.0.0) - webrat (0.7.2.beta.1) - nokogiri (>= 1.2.0) - rack (>= 1.0) - rack-test (>= 0.5.3) will_paginate (3.0.pre2) PLATFORMS @@ -110,13 +103,13 @@ PLATFORMS DEPENDENCIES bson_ext - database_cleaner (= 0.5.2) - devise (= 1.1.1) + database_cleaner (~> 0.6.0) + devise (~> 1.1.3) factory_girl_rails haml libxml-ruby - mongoid (= 2.0.0.beta.15) - rails (= 3.0.0.rc) - rspec (>= 2.0.0.beta.19) - rspec-rails (>= 2.0.0.beta.19) + mongoid (~> 2.0.0.beta.20) + rails (= 3.0.3) + rspec (~> 2.1) + rspec-rails (~> 2.1) will_paginate diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8793dde177..a190415d07 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,8 +18,7 @@ config.alias_example_to :fit, :focused => true config.before(:each) do - DatabaseCleaner.orm = "mongoid" - DatabaseCleaner.strategy = :truncation + DatabaseCleaner[:mongoid].strategy = :truncation DatabaseCleaner.clean end end \ No newline at end of file From 0a406eda477beac506242b37930dce7a3df1c88f Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 23 Nov 2010 15:40:25 +0300 Subject: [PATCH 004/115] Ensure first user is admin. --- db/seeds.rb | 8 +++++--- spec/models/user_spec.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index eb31f043c5..4bfed3d8ac 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,10 +10,12 @@ puts "-- password: #{admin_pass}" puts "" puts "Be sure to change these credentials ASAP!" -User.create!({ +user = User.new({ :name => 'Errbit Admin', :email => admin_email, :password => admin_pass, :password_confirmation => admin_pass, - :admin => true -}) \ No newline at end of file +}) + +user.admin = true +user.save! \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c031589d66..222c7fac72 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -38,5 +38,14 @@ end end + + context "First user" do + it "should be created this admin access via db:seed" do + require 'rake' + Errbit::Application.load_tasks + Rake::Task["db:seed"].execute + User.first.admin.should be_true + end + end end From 55f2636e15ce44df9780b676794f9e79ff480cf2 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 23 Nov 2010 16:02:38 +0300 Subject: [PATCH 005/115] Admin is able to make other admins on user creation. --- app/controllers/users_controller.rb | 3 +++ spec/controllers/users_controller_spec.rb | 28 ++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 70aaa70634..2537355db8 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -23,6 +23,9 @@ def edit def create @user = User.new(params[:user]) + # Set protected attributes + @user.admin = params[:user].try(:[], :admin) if current_user.admin? + if @user.save flash[:success] = "#{@user.name} is now part of the team. Be sure to add them as a project watcher." redirect_to user_path(@user) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index f05a6b3ecc..de8eabecc9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -48,6 +48,11 @@ put :update, :id => @user.to_param, :user => {:name => 'Kermit'} response.should redirect_to(user_path(@user)) end + + it "should not be able to become an admin" do + put :update, :id => @user.to_param, :user => {:admin => true} + @user.reload.admin.should be_false + end end context "when the update is unsuccessful" do @@ -100,19 +105,24 @@ context "POST /users" do context "when the create is successful" do before do - @user = Factory(:user) - User.should_receive(:new).and_return(@user) - @user.should_receive(:save).and_return(true) + @attrs = {:user => Factory.attributes_for(:user)} end it "sets a message to display" do - post :create + post :create, @attrs request.flash[:success].should include('part of the team') end it "redirects to the user's page" do - post :create - response.should redirect_to(user_path(@user)) + post :create, @attrs + response.should redirect_to(user_path(assigns(:user))) + end + + it "should be able to create admin" do + @attrs[:user][:admin] = true + post :create, @attrs + response.should be_redirect + User.find(assigns(:user).to_param).admin.should be_true end end @@ -145,6 +155,12 @@ put :update, :id => @user.to_param, :user => {:name => 'Kermit'} response.should redirect_to(user_path(@user)) end + + it "should be able to make user an admin" do + put :update, :id => @user.to_param, :user => {:admin => true} + response.should be_redirect + User.find(assigns(:user).to_param).admin.should be_true + end end context "when the update is unsuccessful" do From ec51e940b460618b6c0eebec443292712eda07db Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 23 Nov 2010 17:04:06 +0300 Subject: [PATCH 006/115] Index on Errs#last_notice_at should allow listing pile of errors. --- Rakefile | 14 ++++++++++++++ app/models/err.rb | 4 +++- db/seeds.rb | 2 +- lib/tasks/errbit/bootstrap.rake | 2 ++ spec/controllers/errs_controller_spec.rb | 10 +++++++++- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index a7ba95e87a..37764344dc 100644 --- a/Rakefile +++ b/Rakefile @@ -8,4 +8,18 @@ require 'bundler' Errbit::Application.load_tasks Rake::Task[:default].clear + +namespace :spec do + desc "Preparing test env" + task :prepare do + tmp_env = Rails.env + Rails.env = "test" + %w( errbit:bootstrap ).each do |task| + Rake::Task[task].invoke + end + Rails.env = tmp_env + end +end + +Rake::Task["spec"].prerequisites.push("spec:prepare") task :default => ['spec'] \ No newline at end of file diff --git a/app/models/err.rb b/app/models/err.rb index 496c773cd6..e55c3e707a 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -9,7 +9,9 @@ class Err field :fingerprint field :last_notice_at, :type => DateTime field :resolved, :type => Boolean, :default => false - + + index :last_notice_at + referenced_in :app embeds_many :notices diff --git a/db/seeds.rb b/db/seeds.rb index 4bfed3d8ac..bc576ef141 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,7 +10,7 @@ puts "-- password: #{admin_pass}" puts "" puts "Be sure to change these credentials ASAP!" -user = User.new({ +user = User.where(:email => admin_email).first || User.new({ :name => 'Errbit Admin', :email => admin_email, :password => admin_pass, diff --git a/lib/tasks/errbit/bootstrap.rake b/lib/tasks/errbit/bootstrap.rake index 714c09872b..5c9470b02b 100644 --- a/lib/tasks/errbit/bootstrap.rake +++ b/lib/tasks/errbit/bootstrap.rake @@ -26,6 +26,8 @@ namespace :errbit do Rake::Task['errbit:copy_configs'].execute puts "\n" Rake::Task['db:seed'].invoke + puts "\n" + Rake::Task['db:mongoid:create_indexes'].invoke end end \ No newline at end of file diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index f0dc622703..4b0fcadf2e 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -12,8 +12,11 @@ describe "GET /errs" do context 'when logged in as an admin' do - it "gets a paginated list of unresolved errs" do + before(:each) do sign_in Factory(:admin) + end + + it "gets a paginated list of unresolved errs" do errs = WillPaginate::Collection.new(1,30) 3.times { errs << Factory(:err) } Err.should_receive(:unresolved).and_return( @@ -22,6 +25,11 @@ get :index assigns(:errs).should == errs end + + it "should handle lots of errors" do + 1000.times { Factory :notice } + lambda { get :index }.should_not raise_error + end end context 'when logged in as a user' do From 115250ff3e57b8480ae82d022b7d6bb4f4e93d6c Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 23 Nov 2010 17:15:34 +0300 Subject: [PATCH 007/115] Pagination fixed. WillPaginate Mongoid extension requires [per_page] param. --- app/controllers/apps_controller.rb | 2 +- app/controllers/errs_controller.rb | 4 ++-- app/controllers/users_controller.rb | 2 +- app/models/err.rb | 2 ++ app/models/user.rb | 2 ++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index e357d5ad45..2b27d37fb8 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -8,7 +8,7 @@ def index end def show - @errs = @app.errs.paginate + @errs = @app.errs.paginate(:page => params[:page], :per_page => Err.per_page) end def new diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index 12856ac9ed..b4093c0f36 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -4,12 +4,12 @@ class ErrsController < ApplicationController def index app_scope = current_user.admin? ? App.all : current_user.apps - @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page]) + @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page], :per_page => Err.per_page) end def all app_scope = current_user.admin? ? App.all : current_user.apps - @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page]) + @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => Err.per_page) end def show diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2537355db8..932f46d9d4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -6,7 +6,7 @@ class UsersController < ApplicationController before_filter :require_user_edit_priviledges, :only => [:edit, :update] def index - @users = User.paginate(:page => params[:page]) + @users = User.paginate(:page => params[:page], :per_page => User.per_page) end def show diff --git a/app/models/err.rb b/app/models/err.rb index e55c3e707a..3d28c7c2ba 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -1,4 +1,6 @@ class Err + cattr_reader :per_page + @@per_page = 30 include Mongoid::Document include Mongoid::Timestamps diff --git a/app/models/user.rb b/app/models/user.rb index 603d1d1a29..aac2ab7b5f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,6 @@ class User + cattr_reader :per_page + @@per_page = 30 include Mongoid::Document include Mongoid::Timestamps From 4d9bc87ef486611d1e77058c561004bf7abbf90f Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 23 Nov 2010 18:50:05 +0300 Subject: [PATCH 008/115] Pending spec detected. --- spec/controllers/apps_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 2d2d45d17d..f3a70c09ec 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -43,7 +43,7 @@ context 'logged in as a user' do it 'finds the app if the user is watching it' do - + pending end it 'does not find the app if the user is not watching it' do From ef27f84e5998712996e69c3cc39eec4b76febb4f Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 24 Nov 2010 12:59:34 +0300 Subject: [PATCH 009/115] Handle errs without notices. Userful for testing custom clients and different tests. --- app/helpers/errs_helper.rb | 7 ++++ app/models/err.rb | 2 +- app/views/errs/_table.html.haml | 4 +-- app/views/errs/show.html.haml | 41 ++++++++++++------------ spec/controllers/apps_controller_spec.rb | 17 +++++++--- spec/controllers/errs_controller_spec.rb | 10 +++--- 6 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 app/helpers/errs_helper.rb diff --git a/app/helpers/errs_helper.rb b/app/helpers/errs_helper.rb new file mode 100644 index 0000000000..ff56309e88 --- /dev/null +++ b/app/helpers/errs_helper.rb @@ -0,0 +1,7 @@ +module ErrsHelper + + def last_notice_at err + err.last_notice_at || err.created_at + end + +end \ No newline at end of file diff --git a/app/models/err.rb b/app/models/err.rb index 3d28c7c2ba..a43676838a 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -45,7 +45,7 @@ def where end def message - notices.first.message || klass + notices.first.try(:message) || klass end end \ No newline at end of file diff --git a/app/views/errs/_table.html.haml b/app/views/errs/_table.html.haml index 649175a806..06b7017b49 100644 --- a/app/views/errs/_table.html.haml +++ b/app/views/errs/_table.html.haml @@ -15,11 +15,11 @@ %td.message = link_to err.message, app_err_path(err.app, err) %em= err.where - %td.latest #{time_ago_in_words(err.last_notice_at)} ago + %td.latest #{time_ago_in_words(last_notice_at err)} ago %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a' %td.count= link_to err.notices.count, app_err_path(err.app, err) - if errs.none? %tr %td{:colspan => (@app ? 5 : 6)} %em No errs here -= will_paginate @errs, :previous_label => '« Previous', :next_label => 'Next »' \ No newline at end of file += will_paginate @errs, :previous_label => '« Previous', :next_label => 'Next »' diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index 99b47cc5f0..f7e38db0a4 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -6,11 +6,11 @@ %strong Environment: = @err.environment %strong Last Notice: - = @err.last_notice_at.to_s(:micro) + = last_notice_at(@err).to_s(:micro) - content_for :action_bar do %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => 'Seriously?', :class => 'resolve' if @err.unresolved? -%h4= @notice.message +%h4= @notice.try(:message) = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination' viewing occurrence #{@notices.current_page} of #{@notices.total_pages} @@ -23,22 +23,23 @@ viewing occurrence #{@notices.current_page} of #{@notices.total_pages} %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button' %li= link_to 'Session', '#session', :rel => 'session', :class => 'button' -#summary - %h3 Summary - = render 'notices/summary', :notice => @notice - -#backtrace - %h3 Backtrace - = render 'notices/backtrace', :lines => @notice.backtrace +- if @notice + #summary + %h3 Summary + = render 'notices/summary', :notice => @notice -#environment - %h3 Environment - = render 'notices/environment', :notice => @notice - -#params - %h3 Parameters - = render 'notices/params', :notice => @notice - -#session - %h3 Session - = render 'notices/session', :notice => @notice \ No newline at end of file + #backtrace + %h3 Backtrace + = render 'notices/backtrace', :lines => @notice.backtrace + + #environment + %h3 Environment + = render 'notices/environment', :notice => @notice + + #params + %h3 Parameters + = render 'notices/params', :notice => @notice + + #session + %h3 Session + = render 'notices/session', :notice => @notice diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index f3a70c09ec..840e285045 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -32,12 +32,21 @@ end describe "GET /apps/:id" do + render_views context 'logged in as an admin' do - it 'finds the app' do + before(:each) do sign_in Factory(:admin) - app = Factory(:app) - get :show, :id => app.id - assigns(:app).should == app + @app = Factory(:app) + end + + it 'finds the app' do + get :show, :id => @app.id + assigns(:app).should == @app + end + + it "should not raise errors for app with err without notices" do + Factory :err, :app => @app + lambda { get :show, :id => @app.id }.should_not raise_error end end diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 4b0fcadf2e..8f2a77c153 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -74,6 +74,8 @@ end describe "GET /apps/:app_id/errs/:id" do + render_views + before do 3.times { Factory(:notice, :err => err)} end @@ -93,13 +95,9 @@ assigns(:err).should == err end - it "paginates the notices, 1 at a time" do - App.stub(:find).with(app.id).and_return(app) - app.errs.stub(:find).with(err.id).and_return(err) - err.notices.should_receive(:ordered).and_return(proxy = stub('proxy')) - proxy.should_receive(:paginate).with(:page => 3, :per_page => 1). - and_return(WillPaginate::Collection.new(1,1) << err.notices.first) + it "successfully render page" do get :show, :app_id => app.id, :id => err.id + response.should be_success end end From 44a2edcb135b9ba6d39bf26e1f88e259ff19ce89 Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Mon, 20 Dec 2010 10:53:28 -0600 Subject: [PATCH 010/115] skip verify authenticity token on DeploysController#create. --- app/controllers/deploys_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index f026c8901e..17847bf57c 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -1,5 +1,6 @@ class DeploysController < ApplicationController + skip_before_filter :verify_authenticity_token, :only => :create skip_before_filter :authenticate_user!, :only => :create def create From c39f715ed51a599fb02faea805de926ae407476b Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Mon, 20 Dec 2010 11:24:08 -0600 Subject: [PATCH 011/115] Remove lie comment about deploy requiring port 80. --- app/views/apps/_configuration_instructions.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/apps/_configuration_instructions.html.haml b/app/views/apps/_configuration_instructions.html.haml index b210cc6127..62abcf4872 100644 --- a/app/views/apps/_configuration_instructions.html.haml +++ b/app/views/apps/_configuration_instructions.html.haml @@ -16,7 +16,7 @@ HoptoadNotifier.configure do |config| config.api_key = '#{app.api_key}' config.host = '#{request.host}' - config.port = #{request.port} # Note: Deployment notifications only work on port 80 + config.port = #{request.port} end # # Testing From b857e8ac1a1003ee50c8cd986050fcc8c32ad468 Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Mon, 20 Dec 2010 14:51:02 -0600 Subject: [PATCH 012/115] Bug when receiving a hoptoad notice without a request in the params. --- app/models/notice.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/notice.rb b/app/models/notice.rb index 95568f0a3c..694d95cbd9 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -23,6 +23,7 @@ def self.from_xml(hoptoad_xml) hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml) app = App.find_by_api_key!(hoptoad_notice['api-key']) + hoptoad_notice['request'] ||= {} hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank? hoptoad_notice['request']['action'] = nil if hoptoad_notice['request']['action'].blank? From 0651a96c59f69ca2f067462faec5f66866cb7aa7 Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Tue, 4 Jan 2011 14:46:08 -0600 Subject: [PATCH 013/115] Order errors on app pages. --- app/controllers/apps_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 2b27d37fb8..6e57753769 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -8,7 +8,7 @@ def index end def show - @errs = @app.errs.paginate(:page => params[:page], :per_page => Err.per_page) + @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) end def new From 717eec1a27540a51127a9b9c3921222e199f6f7e Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Tue, 4 Jan 2011 17:08:55 -0600 Subject: [PATCH 014/115] Added a view of the last five deploys to the application view Conflicts: app/controllers/apps_controller.rb app/models/deploy.rb --- app/controllers/apps_controller.rb | 3 ++- app/models/deploy.rb | 2 ++ app/views/apps/show.html.haml | 22 +++++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 6e57753769..84533debf0 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -8,7 +8,8 @@ def index end def show - @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + @deploys = @app.deploys.order_by(:created_at.desc).limit(5) end def new diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 9158db5f98..c8b4fac776 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -6,6 +6,8 @@ class Deploy field :repository field :environment field :revision + + index :created_at, Mongo::DESCENDING embedded_in :app, :inverse_of => :deploys diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml index 57c6a813ad..8ef8b0af8f 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -23,9 +23,29 @@ %td %em Sadly, no one is watching this app +%h3 Latest Deploys +- if @deploys.any? + %table.deploys + %thead + %tr + %th When + %th Who + %th Repository + %th Revision + + %tbody + - @deploys.each do |deploy| + %tr + %td.when #{deploy.created_at.to_s(:micro)} + %td.who #{deploy.username} + %td.repository #{deploy.repository} + %td.revision #{deploy.revision} +- else + %h3 No deploys + - if @app.errs.any? %h3 Errs = render 'errs/table', :errs => @errs - else %h3 No errs have been caught yet, make sure you setup your app - = render 'configuration_instructions', :app => @app \ No newline at end of file + = render 'configuration_instructions', :app => @app From 7f5f16cd571f119c2aa0669fa2ee0f2d9bef19f4 Mon Sep 17 00:00:00 2001 From: Chris Cooke Date: Tue, 21 Dec 2010 16:17:01 +0000 Subject: [PATCH 015/115] List all deploys for an app but with no pagination --- app/controllers/deploys_controller.rb | 8 +++++++- app/views/deploys/_table.html.haml | 20 ++++++++++++++++++++ app/views/deploys/index.html.haml | 2 ++ config/routes.rb | 2 ++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/views/deploys/_table.html.haml create mode 100644 app/views/deploys/index.html.haml diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 17847bf57c..5bfe9766f4 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -13,5 +13,11 @@ def create }) render :xml => @deploy end + + def index + app = current_user.apps.find(:conditions => {:name => params[:app_id]}).first + + @deploys = app.deploys.order_by(:created_at.desc) + end -end \ No newline at end of file +end diff --git a/app/views/deploys/_table.html.haml b/app/views/deploys/_table.html.haml new file mode 100644 index 0000000000..6a70badb9c --- /dev/null +++ b/app/views/deploys/_table.html.haml @@ -0,0 +1,20 @@ +%table.errs + %thead + %tr + %th App + %th When + %th Who + %th Message + %th Repository + %th Revision + %tbody + - deploys.each do |deploy| + %tr + %td.app + = deploy.app.name + %span.environment= deploy.environment + %td.latest #{time_ago_in_words(deploy.created_at)} ago + %td.who #{deploy.username} + %td.message #{deploy.message} + %td.repository #{deploy.repository} + %td.revision #{deploy.revision} diff --git a/app/views/deploys/index.html.haml b/app/views/deploys/index.html.haml new file mode 100644 index 0000000000..4434ca9823 --- /dev/null +++ b/app/views/deploys/index.html.haml @@ -0,0 +1,2 @@ +- content_for :title, 'Deploys' += render 'table', :deploys => @deploys diff --git a/config/routes.rb b/config/routes.rb index 9f4cd9aacd..33afb06a2f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,8 @@ put :resolve end end + + resources :deploys, :only => [:index] end devise_for :users From c3043666fe7e71e3f13355063cc75da23a1457ab Mon Sep 17 00:00:00 2001 From: Chris Cooke Date: Tue, 21 Dec 2010 16:30:36 +0000 Subject: [PATCH 016/115] Show number of deploys and link to deploys page --- app/views/apps/show.html.haml | 7 +++++-- public/stylesheets/application.css | 8 +++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml index 8ef8b0af8f..5c70b10c39 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -2,6 +2,8 @@ - content_for :meta do %strong Errs Caught: = @app.errs.count + %strong Deploy Count: + = @app.deploys.count %strong API Key: = @app.api_key - content_for :action_bar do @@ -40,12 +42,13 @@ %td.who #{deploy.username} %td.repository #{deploy.repository} %td.revision #{deploy.revision} + = link_to "All Deploys (#{@app.deploys.count})", app_deploys_path(@app), :class => 'button' - else %h3 No deploys - if @app.errs.any? - %h3 Errs + %h3.clear Errs = render 'errs/table', :errs => @errs - else - %h3 No errs have been caught yet, make sure you setup your app + %h3.clear No errs have been caught yet, make sure you setup your app = render 'configuration_instructions', :app => @app diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 03ef5db6fd..6d3a74adc5 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -195,6 +195,12 @@ a.action { float: right; font-size: 0.9em;} padding: 20px; border-top: 1px solid #C6C6C6; background-color: #FFF; } + +#content a.button { + float: right; + display: block; + margin-bottom: 10px; +} /* Footer */ #footer { @@ -605,4 +611,4 @@ table.backtrace li { table.backtrace li.in-app { color: #2adb2e; background-color: #2f2f2f; -} \ No newline at end of file +} From d5995c47ea9b935f5eff43abaad7b94484f531a3 Mon Sep 17 00:00:00 2001 From: Chris Cooke Date: Tue, 21 Dec 2010 16:33:27 +0000 Subject: [PATCH 017/115] Add link from the application page to the deploys --- app/views/apps/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/apps/index.html.haml b/app/views/apps/index.html.haml index 3916553af4..c636d814bc 100644 --- a/app/views/apps/index.html.haml +++ b/app/views/apps/index.html.haml @@ -12,7 +12,7 @@ - @apps.each do |app| %tr %td.name= link_to app.name, app_path(app) - %td.deploy= app.last_deploy_at ? app.last_deploy_at.to_s(:micro) : 'n/a' + %td.deploy= app.last_deploy_at ? link_to( app.last_deploy_at.to_s(:micro), app_deploys_path(app)) : 'n/a' %td.count - if app.errs.any? = link_to app.errs.unresolved.count, app_errs_path(app) @@ -23,4 +23,4 @@ %td{:colspan => 3} %em No apps here. - = link_to 'Click here to create your first one', new_app_path \ No newline at end of file + = link_to 'Click here to create your first one', new_app_path From a162ef00465022200b7195d65ef78adb003e6371 Mon Sep 17 00:00:00 2001 From: Chris Cooke Date: Tue, 21 Dec 2010 16:40:07 +0000 Subject: [PATCH 018/115] Limit the deploys list to 10 and paginate --- app/controllers/deploys_controller.rb | 2 +- app/views/deploys/index.html.haml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 5bfe9766f4..30f6811a79 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -17,7 +17,7 @@ def create def index app = current_user.apps.find(:conditions => {:name => params[:app_id]}).first - @deploys = app.deploys.order_by(:created_at.desc) + @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10) end end diff --git a/app/views/deploys/index.html.haml b/app/views/deploys/index.html.haml index 4434ca9823..8d99fa4dfd 100644 --- a/app/views/deploys/index.html.haml +++ b/app/views/deploys/index.html.haml @@ -1,2 +1,3 @@ - content_for :title, 'Deploys' = render 'table', :deploys => @deploys += will_paginate @deploys, :previous_label => '« Previous', :next_label => 'Next »' From 0984139a07471b0cafa97074a938a7e14ef6a29c Mon Sep 17 00:00:00 2001 From: Chris Cooke Date: Tue, 21 Dec 2010 16:42:05 +0000 Subject: [PATCH 019/115] This is now done. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index fa7cc720bc..9aa0e89ad5 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at TODO ---- -* Add a deployment view * Add ability for watchers to be configured for types of notifications they should receive Special Thanks From 7f883b128a766f303f0b5c45143b493c904f2651 Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Tue, 4 Jan 2011 17:20:02 -0600 Subject: [PATCH 020/115] Fix bug in finding app for deployment index screen. --- app/controllers/deploys_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 30f6811a79..225bd8e65e 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -15,7 +15,7 @@ def create end def index - app = current_user.apps.find(:conditions => {:name => params[:app_id]}).first + app = current_user.apps.find(params[:app_id]).first @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10) end From c49ea05e7adf235f8e9091f053e1bd5e7c1409e6 Mon Sep 17 00:00:00 2001 From: Barry Hess Date: Tue, 4 Jan 2011 20:05:10 -0600 Subject: [PATCH 021/115] Actually fix bug in finding app for dpeloying index screen. For real. --- app/controllers/deploys_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 225bd8e65e..361a125413 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -15,7 +15,9 @@ def create end def index - app = current_user.apps.find(params[:app_id]).first + # See AppsController#find_app for the reasoning behind this code. + app = App.find(params[:app_id]) + raise(Mongoid::Errors::DocumentNotFound.new(App,app.id)) unless current_user.admin? || current_user.watching?(app) @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10) end From 3520a42e30a3808d6873374107269d6fdf337753 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Thu, 13 Jan 2011 00:28:43 +0300 Subject: [PATCH 022/115] Spec for notice without a request in the params. --- ...ad_test_notice_without_request_section.xml | 92 +++++++++++++++++++ spec/models/notice_spec.rb | 5 + 2 files changed, 97 insertions(+) create mode 100644 spec/fixtures/hoptoad_test_notice_without_request_section.xml diff --git a/spec/fixtures/hoptoad_test_notice_without_request_section.xml b/spec/fixtures/hoptoad_test_notice_without_request_section.xml new file mode 100644 index 0000000000..d15efda8e4 --- /dev/null +++ b/spec/fixtures/hoptoad_test_notice_without_request_section.xml @@ -0,0 +1,92 @@ + + + APIKEY + + Hoptoad Notifier + 2.3.2 + http://hoptoadapp.com + + + HoptoadTestingException + HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /path/to/sample/project + development + + \ No newline at end of file diff --git a/spec/models/notice_spec.rb b/spec/models/notice_spec.rb index e586c5e565..2d89542107 100644 --- a/spec/models/notice_spec.rb +++ b/spec/models/notice_spec.rb @@ -98,6 +98,11 @@ @notice = Notice.from_xml(@xml) @notice.notifier['name'].should == 'Hoptoad Notifier' end + + it "should handle params withour 'request' section" do + @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read + lambda { Notice.from_xml(@xml) }.should_not raise_error + end end describe "email notifications" do From aa6c9cac7b5039c6be389cb9e511bdef825c3688 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Thu, 13 Jan 2011 01:06:13 +0300 Subject: [PATCH 023/115] Moving factory-girl sequences to factories.rb --- spec/factories.rb | 4 ++++ spec/factories/app_factories.rb | 5 +---- spec/factories/user_factories.rb | 2 -- 3 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 spec/factories.rb diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 0000000000..66fbae2390 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,4 @@ +Factory.sequence(:name) {|n| "John #{n} Doe"} +Factory.sequence(:app_name) {|n| "App ##{n}"} +Factory.sequence(:email) {|n| "email#{n}@example.com"} +Factory.sequence(:user_email) {|n| "user.#{n}@example.com"} diff --git a/spec/factories/app_factories.rb b/spec/factories/app_factories.rb index ea72f41135..5f655649ab 100644 --- a/spec/factories/app_factories.rb +++ b/spec/factories/app_factories.rb @@ -1,6 +1,3 @@ -Factory.sequence(:app_name) {|n| "App ##{n}"} -Factory.sequence(:email) {|n| "email#{n}@example.com"} - Factory.define(:app) do |p| p.name { Factory.next :app_name } end @@ -27,5 +24,5 @@ d.username 'clyde.frog' d.repository 'git@github.com/jdpace/errbit.git' d.environment 'production' - d.revision '2e601cb575ca97f1a1097f12d0edfae241a70263' + d.revision ActiveSupport::SecureRandom.hex(10) end \ No newline at end of file diff --git a/spec/factories/user_factories.rb b/spec/factories/user_factories.rb index ce2af3f430..72ea8c63c9 100644 --- a/spec/factories/user_factories.rb +++ b/spec/factories/user_factories.rb @@ -1,5 +1,3 @@ -Factory.sequence(:user_email) {|n| "user.#{n}@example.com"} - Factory.define :user do |u| u.name 'Clyde Frog' u.email { Factory.next :user_email } From 943c836a9f6234faf65c44c1d62b22c4e676fffa Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Thu, 13 Jan 2011 01:25:36 +0300 Subject: [PATCH 024/115] Specs for DeployController#index. --- app/views/deploys/_table.html.haml | 2 -- spec/controllers/deploys_controller_spec.rb | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/views/deploys/_table.html.haml b/app/views/deploys/_table.html.haml index 6a70badb9c..18ecb5ec6f 100644 --- a/app/views/deploys/_table.html.haml +++ b/app/views/deploys/_table.html.haml @@ -4,7 +4,6 @@ %th App %th When %th Who - %th Message %th Repository %th Revision %tbody @@ -15,6 +14,5 @@ %span.environment= deploy.environment %td.latest #{time_ago_in_words(deploy.created_at)} ago %td.who #{deploy.username} - %td.message #{deploy.message} %td.repository #{deploy.repository} %td.revision #{deploy.revision} diff --git a/spec/controllers/deploys_controller_spec.rb b/spec/controllers/deploys_controller_spec.rb index fe77f6f7ba..9578ae390a 100644 --- a/spec/controllers/deploys_controller_spec.rb +++ b/spec/controllers/deploys_controller_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe DeploysController do + render_views context 'POST #create' do before do @@ -38,5 +39,22 @@ end end + + context "GET #index" do + before(:each) do + @deploy = Factory :deploy + sign_in Factory(:admin) + get :index, :app_id => @deploy.app.id + end + + it "should render successfully" do + response.should be_success + end + + it "should contain info about existing deploy" do + response.body.should match(@deploy.revision) + response.body.should match(@deploy.app.name) + end + end end \ No newline at end of file From d55d2edf8aa2c95c28990f883b305c0c9b457579 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Thu, 13 Jan 2011 10:52:33 +0300 Subject: [PATCH 025/115] Bundler was released long time ago. Fixing readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aa0e89ad5..4d0a0567c9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at 3. Install Bundler - gem install bundler --pre + gem install bundler **Running Locally:** From 11173e8143c643011d3007999964cb09f3407a11 Mon Sep 17 00:00:00 2001 From: Jared Pace Date: Sat, 5 Feb 2011 10:27:58 -0500 Subject: [PATCH 026/115] Update mongoid to latest RC --- Gemfile | 4 ++-- Gemfile.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 82b710ee88..b359c453e3 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,8 @@ source 'http://rubygems.org' gem 'rails', '3.0.3' gem 'libxml-ruby' -gem 'bson_ext', :require => nil -gem 'mongoid', '~> 2.0.0.beta.20' +gem 'bson_ext', '~> 1.2' +gem 'mongoid', '~> 2.0.0.rc.6' gem 'haml' gem 'will_paginate' gem 'devise', '~> 1.1.3' diff --git a/Gemfile.lock b/Gemfile.lock index 57d8dab63e..eb83b952f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,7 +31,7 @@ GEM arel (2.0.4) bcrypt-ruby (2.1.2) bson (1.1.2) - bson_ext (1.1.2) + bson_ext (1.2.0) builder (2.1.2) database_cleaner (0.6.0) devise (1.1.3) @@ -102,13 +102,13 @@ PLATFORMS ruby DEPENDENCIES - bson_ext + bson_ext (~> 1.2) database_cleaner (~> 0.6.0) devise (~> 1.1.3) factory_girl_rails haml libxml-ruby - mongoid (~> 2.0.0.beta.20) + mongoid (~> 2.0.0.rc.6) rails (= 3.0.3) rspec (~> 2.1) rspec-rails (~> 2.1) From 49311ba61e4f024c2f1d26574c276b1374fb73fe Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Sat, 19 Feb 2011 11:47:46 +0100 Subject: [PATCH 027/115] Deploy notifications needs POST method --- config/routes.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 9f4cd9aacd..b3fe030f0e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,11 @@ Errbit::Application.routes.draw do - + devise_for :users # Hoptoad Notifier Routes match '/notifier_api/v2/notices' => 'notices#create' - match '/deploys.txt' => 'deploys#create' - + match '/deploys.txt' => 'deploys#create', :via => [:get, :post] + resources :notices, :only => [:show] resources :deploys, :only => [:show] resources :users @@ -14,7 +14,7 @@ get :all end end - + resources :apps do resources :errs do resources :notices @@ -23,9 +23,9 @@ end end end - + devise_for :users - + root :to => 'apps#index' - + end From 1a1c0aeecdf50177bc3148f1b1ac51dae1f7aa69 Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Sat, 19 Feb 2011 11:54:11 +0100 Subject: [PATCH 028/115] Trying to fix deploy error --- app/controllers/deploys_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index f026c8901e..bc7eb91f95 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -1,7 +1,8 @@ class DeploysController < ApplicationController - + + protect_from_forgery :except => [ :create ] skip_before_filter :authenticate_user!, :only => :create - + def create @app = App.find_by_api_key!(params[:api_key]) @deploy = @app.deploys.create!({ @@ -12,5 +13,5 @@ def create }) render :xml => @deploy end - + end \ No newline at end of file From 9cc85a24528b371cbf88e11e3263884c4c00a324 Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Sat, 19 Feb 2011 12:01:05 +0100 Subject: [PATCH 029/115] Fixing the ActionController::InvalidAuthenticityToken error --- app/controllers/deploys_controller.rb | 7 ++++--- config/routes.rb | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index f026c8901e..bc7eb91f95 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -1,7 +1,8 @@ class DeploysController < ApplicationController - + + protect_from_forgery :except => [ :create ] skip_before_filter :authenticate_user!, :only => :create - + def create @app = App.find_by_api_key!(params[:api_key]) @deploy = @app.deploys.create!({ @@ -12,5 +13,5 @@ def create }) render :xml => @deploy end - + end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 9f4cd9aacd..cfc147b4a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,11 @@ Errbit::Application.routes.draw do - + devise_for :users # Hoptoad Notifier Routes match '/notifier_api/v2/notices' => 'notices#create' - match '/deploys.txt' => 'deploys#create' - + match '/deploys.txt' => 'deploys#create', :via => [:post] + resources :notices, :only => [:show] resources :deploys, :only => [:show] resources :users @@ -14,7 +14,7 @@ get :all end end - + resources :apps do resources :errs do resources :notices @@ -23,9 +23,9 @@ end end end - + devise_for :users - + root :to => 'apps#index' - + end From 117baf005cd3308336f468680f33f57774ebd061 Mon Sep 17 00:00:00 2001 From: Raja Bhadury Date: Mon, 21 Feb 2011 17:25:27 +0000 Subject: [PATCH 030/115] Turn on/off email notifications for deploys and errors on a per-application basis. Allow message with deploy notifications (so we know what is was for) --- app/controllers/deploys_controller.rb | 5 ++++- app/models/app.rb | 2 ++ app/models/deploy.rb | 3 ++- app/models/err.rb | 1 + app/models/notice.rb | 2 +- app/views/apps/_fields.html.haml | 10 +++++++++- app/views/apps/show.html.haml | 2 ++ app/views/deploys/_table.html.haml | 2 ++ 8 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 361a125413..0ff1afd0c6 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -1,4 +1,6 @@ class DeploysController < ApplicationController + + protect_from_forgery :except => :create skip_before_filter :verify_authenticity_token, :only => :create skip_before_filter :authenticate_user!, :only => :create @@ -9,7 +11,8 @@ def create :username => params[:deploy][:local_username], :environment => params[:deploy][:rails_env], :repository => params[:deploy][:scm_repository], - :revision => params[:deploy][:scm_revision] + :revision => params[:deploy][:scm_revision], + :message => params[:deploy][:message] }) render :xml => @deploy end diff --git a/app/models/app.rb b/app/models/app.rb index 42c317ad18..28ce850bce 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -5,6 +5,8 @@ class App field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false + field :notify_on_errs, :type => Boolean, :default => false + field :notify_on_deploys, :type => Boolean, :default => false key :name embeds_many :watchers diff --git a/app/models/deploy.rb b/app/models/deploy.rb index c8b4fac776..3e9a79010a 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -6,6 +6,7 @@ class Deploy field :repository field :environment field :revision + field :message index :created_at, Mongo::DESCENDING @@ -27,7 +28,7 @@ def resolve_app_errs protected def should_notify? - app.watchers.any? + app.notify_on_deploys? && app.watchers.any? end def should_resolve_app_errs? diff --git a/app/models/err.rb b/app/models/err.rb index a43676838a..93ed290db0 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -13,6 +13,7 @@ class Err field :resolved, :type => Boolean, :default => false index :last_notice_at + index :app_id referenced_in :app embeds_many :notices diff --git a/app/models/notice.rb b/app/models/notice.rb index 694d95cbd9..83b5ce1663 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -73,7 +73,7 @@ def cache_last_notice_at protected def should_notify? - Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? + err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? end end \ No newline at end of file diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index 0a2c82031e..d9d0596cef 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -4,10 +4,18 @@ = f.label :name = f.text_field :name +%div.checkbox + = f.check_box :notify_on_errs + = f.label :notify_on_errs, 'Notify on errors' + %div.checkbox = f.check_box :resolve_errs_on_deploy = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy' - + +%div.checkbox + = f.check_box :notify_on_deploys + = f.label :notify_on_deploys, 'Notify on deploys' + %fieldset.nested-wrapper %legend Watchers - f.fields_for :watchers do |w| diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml index 5c70b10c39..f4d40b6a78 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -32,6 +32,7 @@ %tr %th When %th Who + %th Message %th Repository %th Revision @@ -40,6 +41,7 @@ %tr %td.when #{deploy.created_at.to_s(:micro)} %td.who #{deploy.username} + %td.message #{deploy.message} %td.repository #{deploy.repository} %td.revision #{deploy.revision} = link_to "All Deploys (#{@app.deploys.count})", app_deploys_path(@app), :class => 'button' diff --git a/app/views/deploys/_table.html.haml b/app/views/deploys/_table.html.haml index 18ecb5ec6f..6a70badb9c 100644 --- a/app/views/deploys/_table.html.haml +++ b/app/views/deploys/_table.html.haml @@ -4,6 +4,7 @@ %th App %th When %th Who + %th Message %th Repository %th Revision %tbody @@ -14,5 +15,6 @@ %span.environment= deploy.environment %td.latest #{time_ago_in_words(deploy.created_at)} ago %td.who #{deploy.username} + %td.message #{deploy.message} %td.repository #{deploy.repository} %td.revision #{deploy.revision} From 68e0d61fb8da83c52e1cc483cc25c9bebdcae10c Mon Sep 17 00:00:00 2001 From: Raja Bhadury Date: Mon, 21 Feb 2011 17:25:27 +0000 Subject: [PATCH 031/115] Turn on/off email notifications for deploys and errors on a per-application basis. Allow message with deploy notifications (so we know what is was for). Add index to errors to speed up viewing them. --- app/controllers/deploys_controller.rb | 5 ++++- app/models/app.rb | 2 ++ app/models/deploy.rb | 3 ++- app/models/err.rb | 1 + app/models/notice.rb | 2 +- app/views/apps/_fields.html.haml | 10 +++++++++- app/views/apps/show.html.haml | 2 ++ app/views/deploys/_table.html.haml | 2 ++ 8 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 361a125413..0ff1afd0c6 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -1,4 +1,6 @@ class DeploysController < ApplicationController + + protect_from_forgery :except => :create skip_before_filter :verify_authenticity_token, :only => :create skip_before_filter :authenticate_user!, :only => :create @@ -9,7 +11,8 @@ def create :username => params[:deploy][:local_username], :environment => params[:deploy][:rails_env], :repository => params[:deploy][:scm_repository], - :revision => params[:deploy][:scm_revision] + :revision => params[:deploy][:scm_revision], + :message => params[:deploy][:message] }) render :xml => @deploy end diff --git a/app/models/app.rb b/app/models/app.rb index 42c317ad18..28ce850bce 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -5,6 +5,8 @@ class App field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false + field :notify_on_errs, :type => Boolean, :default => false + field :notify_on_deploys, :type => Boolean, :default => false key :name embeds_many :watchers diff --git a/app/models/deploy.rb b/app/models/deploy.rb index c8b4fac776..3e9a79010a 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -6,6 +6,7 @@ class Deploy field :repository field :environment field :revision + field :message index :created_at, Mongo::DESCENDING @@ -27,7 +28,7 @@ def resolve_app_errs protected def should_notify? - app.watchers.any? + app.notify_on_deploys? && app.watchers.any? end def should_resolve_app_errs? diff --git a/app/models/err.rb b/app/models/err.rb index a43676838a..93ed290db0 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -13,6 +13,7 @@ class Err field :resolved, :type => Boolean, :default => false index :last_notice_at + index :app_id referenced_in :app embeds_many :notices diff --git a/app/models/notice.rb b/app/models/notice.rb index 694d95cbd9..83b5ce1663 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -73,7 +73,7 @@ def cache_last_notice_at protected def should_notify? - Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? + err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? end end \ No newline at end of file diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index 0a2c82031e..d9d0596cef 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -4,10 +4,18 @@ = f.label :name = f.text_field :name +%div.checkbox + = f.check_box :notify_on_errs + = f.label :notify_on_errs, 'Notify on errors' + %div.checkbox = f.check_box :resolve_errs_on_deploy = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy' - + +%div.checkbox + = f.check_box :notify_on_deploys + = f.label :notify_on_deploys, 'Notify on deploys' + %fieldset.nested-wrapper %legend Watchers - f.fields_for :watchers do |w| diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml index 5c70b10c39..f4d40b6a78 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -32,6 +32,7 @@ %tr %th When %th Who + %th Message %th Repository %th Revision @@ -40,6 +41,7 @@ %tr %td.when #{deploy.created_at.to_s(:micro)} %td.who #{deploy.username} + %td.message #{deploy.message} %td.repository #{deploy.repository} %td.revision #{deploy.revision} = link_to "All Deploys (#{@app.deploys.count})", app_deploys_path(@app), :class => 'button' diff --git a/app/views/deploys/_table.html.haml b/app/views/deploys/_table.html.haml index 18ecb5ec6f..6a70badb9c 100644 --- a/app/views/deploys/_table.html.haml +++ b/app/views/deploys/_table.html.haml @@ -4,6 +4,7 @@ %th App %th When %th Who + %th Message %th Repository %th Revision %tbody @@ -14,5 +15,6 @@ %span.environment= deploy.environment %td.latest #{time_ago_in_words(deploy.created_at)} ago %td.who #{deploy.username} + %td.message #{deploy.message} %td.repository #{deploy.repository} %td.revision #{deploy.revision} From 5d98ec01ca6284a5ff24e0693d8178d49ad3c039 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Thu, 24 Feb 2011 19:45:23 +0300 Subject: [PATCH 032/115] Updating Gemfile.lock to current Gemfile. --- Gemfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb83b952f3..36625c3478 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,8 +30,8 @@ GEM activesupport (3.0.3) arel (2.0.4) bcrypt-ruby (2.1.2) - bson (1.1.2) - bson_ext (1.2.0) + bson (1.2.4) + bson_ext (1.2.4) builder (2.1.2) database_cleaner (0.6.0) devise (1.1.3) @@ -53,11 +53,11 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.16) - mongo (1.1.2) - bson (>= 1.1.1) - mongoid (2.0.0.beta.20) + mongo (1.2.4) + bson (>= 1.2.4) + mongoid (2.0.0.rc.7) activemodel (~> 3.0) - mongo (~> 1.1) + mongo (~> 1.2) tzinfo (~> 0.3.22) will_paginate (~> 3.0.pre) polyglot (0.3.1) @@ -93,7 +93,7 @@ GEM thor (0.14.6) treetop (1.4.9) polyglot (>= 0.3.1) - tzinfo (0.3.23) + tzinfo (0.3.24) warden (0.10.7) rack (>= 1.0.0) will_paginate (3.0.pre2) From e7bd852175c72fe9069b6d1f90e9af3492e18139 Mon Sep 17 00:00:00 2001 From: Philip Hallstrom Date: Thu, 24 Feb 2011 14:22:17 -0800 Subject: [PATCH 033/115] Add configuration option to suppress confirmation when resolving errors. --- app/views/errs/show.html.haml | 2 +- config/config.example.yml | 6 +++- spec/views/errs/show.html.haml_spec.rb | 39 ++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 spec/views/errs/show.html.haml_spec.rb diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index f7e38db0a4..bcf1e6eec3 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -8,7 +8,7 @@ %strong Last Notice: = last_notice_at(@err).to_s(:micro) - content_for :action_bar do - %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => 'Seriously?', :class => 'resolve' if @err.unresolved? + %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => (Errbit::Config.confirm_resolve_err === false ? nil : 'Seriously?'), :class => 'resolve' if @err.unresolved? %h4= @notice.try(:message) diff --git a/config/config.example.yml b/config/config.example.yml index a2a4e46d46..c9b42cbc9b 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -17,4 +17,8 @@ email_from: errbit@example.com # Configure when emails are sent for an error. # [1,3,7] = 1st, 3rd, and 7th occurence triggers # an email notification. -email_at_notices: [1, 10, 100] \ No newline at end of file +email_at_notices: [1, 10, 100] + +# Set to false to suppress confirmation when +# resolving errors. +confirm_resolve_err: true diff --git a/spec/views/errs/show.html.haml_spec.rb b/spec/views/errs/show.html.haml_spec.rb new file mode 100644 index 0000000000..51cfee7d44 --- /dev/null +++ b/spec/views/errs/show.html.haml_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe "errs/show.html.erb" do + before do + err = Factory(:err) + assign :err, err + assign :app, err.app + assign :notices, err.notices.ordered.paginate(:page => 1, :per_page => 1) + assign :notice, err.notices.first + end + + describe "content_for :action_bar" do + + it "should confirm the 'resolve' link by default" do + render + action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar]) + resolve_link = action_bar.match(/()/)[0] + resolve_link.should =~ /data-confirm="Seriously\?"/ + end + + it "should confirm the 'resolve' link if configuration is unset" do + Errbit::Config.stub(:confirm_resolve_err).and_return(nil) + render + action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar]) + resolve_link = action_bar.match(/()/)[0] + resolve_link.should =~ /data-confirm="Seriously\?"/ + end + + it "should not confirm the 'resolve' link if configured not to" do + Errbit::Config.stub(:confirm_resolve_err).and_return(false) + render + action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar]) + resolve_link = action_bar.match(/()/)[0] + resolve_link.should_not =~ /data-confirm=/ + end + + end + +end From 736da21e33292fcd1799e4d61342811f08ac860e Mon Sep 17 00:00:00 2001 From: Steve Sloan Date: Thu, 24 Feb 2011 14:22:06 -0800 Subject: [PATCH 034/115] Support Heroku's HTTP Post deployhook. --- app/controllers/deploys_controller.rb | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/controllers/deploys_controller.rb b/app/controllers/deploys_controller.rb index 361a125413..2c2f2315d7 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -5,12 +5,24 @@ class DeploysController < ApplicationController def create @app = App.find_by_api_key!(params[:api_key]) - @deploy = @app.deploys.create!({ - :username => params[:deploy][:local_username], - :environment => params[:deploy][:rails_env], - :repository => params[:deploy][:scm_repository], - :revision => params[:deploy][:scm_revision] - }) + if params[:deploy] + deploy = { + :username => params[:deploy][:local_username], + :environment => params[:deploy][:rails_env], + :repository => params[:deploy][:scm_repository], + :revision => params[:deploy][:scm_revision], + } + end + + # handle Heroku's HTTP post deployhook format + deploy ||= { + :username => params[:user], + :environment => params[:rack_env].try(:downcase) || params[:app], + :repository => "git@heroku.com:#{params[:app]}.git", + :revision => params[:head], + } + + @deploy = @app.deploys.create!(deploy) render :xml => @deploy end From 3c78afceba557b2af1d4f0a69c5b2adfa7a3a067 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 2 Mar 2011 16:58:11 +0300 Subject: [PATCH 035/115] Gemfile.lock removed to able platform switch. --- .gitignore | 1 + Gemfile.lock | 115 --------------------------------------------------- 2 files changed, 1 insertion(+), 115 deletions(-) delete mode 100644 Gemfile.lock diff --git a/.gitignore b/.gitignore index 6137e75503..33a647d6ad 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ config/deploy.rb config/mongoid.yml .rvmrc *~ +Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 36625c3478..0000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,115 +0,0 @@ -GEM - remote: http://rubygems.org/ - specs: - abstract (1.0.0) - actionmailer (3.0.3) - actionpack (= 3.0.3) - mail (~> 2.2.9) - actionpack (3.0.3) - activemodel (= 3.0.3) - activesupport (= 3.0.3) - builder (~> 2.1.2) - erubis (~> 2.6.6) - i18n (~> 0.4) - rack (~> 1.2.1) - rack-mount (~> 0.6.13) - rack-test (~> 0.5.6) - tzinfo (~> 0.3.23) - activemodel (3.0.3) - activesupport (= 3.0.3) - builder (~> 2.1.2) - i18n (~> 0.4) - activerecord (3.0.3) - activemodel (= 3.0.3) - activesupport (= 3.0.3) - arel (~> 2.0.2) - tzinfo (~> 0.3.23) - activeresource (3.0.3) - activemodel (= 3.0.3) - activesupport (= 3.0.3) - activesupport (3.0.3) - arel (2.0.4) - bcrypt-ruby (2.1.2) - bson (1.2.4) - bson_ext (1.2.4) - builder (2.1.2) - database_cleaner (0.6.0) - devise (1.1.3) - bcrypt-ruby (~> 2.1.2) - warden (~> 0.10.7) - diff-lcs (1.1.2) - erubis (2.6.6) - abstract (>= 1.0.0) - factory_girl (1.3.2) - factory_girl_rails (1.0) - factory_girl (~> 1.3) - rails (>= 3.0.0.beta4) - haml (3.0.24) - i18n (0.4.2) - libxml-ruby (1.1.4) - mail (2.2.10) - activesupport (>= 2.3.6) - i18n (~> 0.4.1) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.16) - mongo (1.2.4) - bson (>= 1.2.4) - mongoid (2.0.0.rc.7) - activemodel (~> 3.0) - mongo (~> 1.2) - tzinfo (~> 0.3.22) - will_paginate (~> 3.0.pre) - polyglot (0.3.1) - rack (1.2.1) - rack-mount (0.6.13) - rack (>= 1.0.0) - rack-test (0.5.6) - rack (>= 1.0) - rails (3.0.3) - actionmailer (= 3.0.3) - actionpack (= 3.0.3) - activerecord (= 3.0.3) - activeresource (= 3.0.3) - activesupport (= 3.0.3) - bundler (~> 1.0) - railties (= 3.0.3) - railties (3.0.3) - actionpack (= 3.0.3) - activesupport (= 3.0.3) - rake (>= 0.8.7) - thor (~> 0.14.4) - rake (0.8.7) - rspec (2.1.0) - rspec-core (~> 2.1.0) - rspec-expectations (~> 2.1.0) - rspec-mocks (~> 2.1.0) - rspec-core (2.1.0) - rspec-expectations (2.1.0) - diff-lcs (~> 1.1.2) - rspec-mocks (2.1.0) - rspec-rails (2.1.0) - rspec (~> 2.1.0) - thor (0.14.6) - treetop (1.4.9) - polyglot (>= 0.3.1) - tzinfo (0.3.24) - warden (0.10.7) - rack (>= 1.0.0) - will_paginate (3.0.pre2) - -PLATFORMS - ruby - -DEPENDENCIES - bson_ext (~> 1.2) - database_cleaner (~> 0.6.0) - devise (~> 1.1.3) - factory_girl_rails - haml - libxml-ruby - mongoid (~> 2.0.0.rc.6) - rails (= 3.0.3) - rspec (~> 2.1) - rspec-rails (~> 2.1) - will_paginate From 668e1464f5910e326b88da072a6b3cabbf8cef43 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 2 Mar 2011 17:15:32 +0300 Subject: [PATCH 036/115] Switching to Nokogiri parser since it suite for both java and mri platforms. --- Gemfile | 2 +- config/initializers/xml_backend.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index b359c453e3..aae456a02f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'http://rubygems.org' gem 'rails', '3.0.3' -gem 'libxml-ruby' +gem 'nokogiri' gem 'bson_ext', '~> 1.2' gem 'mongoid', '~> 2.0.0.rc.6' gem 'haml' diff --git a/config/initializers/xml_backend.rb b/config/initializers/xml_backend.rb index fe1e9883da..3bc16acbdd 100644 --- a/config/initializers/xml_backend.rb +++ b/config/initializers/xml_backend.rb @@ -1 +1 @@ -ActiveSupport::XmlMini.backend = 'LibXML' \ No newline at end of file +ActiveSupport::XmlMini.backend = 'Nokogiri' \ No newline at end of file From b329dcd02e35d6df570f0ddaa3a45c706949d9a6 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 2 Mar 2011 17:18:48 +0300 Subject: [PATCH 037/115] bson_ext will not work on java platform. --- Gemfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index aae456a02f..0d8d436cb9 100644 --- a/Gemfile +++ b/Gemfile @@ -2,12 +2,15 @@ source 'http://rubygems.org' gem 'rails', '3.0.3' gem 'nokogiri' -gem 'bson_ext', '~> 1.2' gem 'mongoid', '~> 2.0.0.rc.6' gem 'haml' gem 'will_paginate' gem 'devise', '~> 1.1.3' +platform :ruby do + gem 'bson_ext', '~> 1.2' +end + group :development, :test do gem 'rspec-rails', '~> 2.1' end From dbde149fbab93e9580266ac64574edcb710b6391 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Fri, 4 Mar 2011 15:36:20 +0300 Subject: [PATCH 038/115] Resolve button in the errs list. --- app/controllers/errs_controller.rb | 5 ++++- app/views/errs/_table.html.haml | 2 ++ public/images/thumbs-up.png | Bin 0 -> 1454 bytes spec/controllers/errs_controller_spec.rb | 8 +++++++- 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 public/images/thumbs-up.png diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index b4093c0f36..e2644ee8e5 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -29,7 +29,10 @@ def resolve @err.resolve! flash[:success] = 'Great news everyone! The err has been resolved.' - redirect_to errs_path + + redirect_to :back + rescue ActionController::RedirectBackError + redirect_to app_path(@app) end protected diff --git a/app/views/errs/_table.html.haml b/app/views/errs/_table.html.haml index 06b7017b49..3bd09f352d 100644 --- a/app/views/errs/_table.html.haml +++ b/app/views/errs/_table.html.haml @@ -6,6 +6,7 @@ %th Latest %th Deploy %th Count + %th Resolve %tbody - errs.each do |err| %tr{:class => err.resolved? ? 'resolved' : 'unresolved'} @@ -18,6 +19,7 @@ %td.latest #{time_ago_in_words(last_notice_at err)} ago %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a' %td.count= link_to err.notices.count, app_err_path(err.app, err) + %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(err.app, err), :title => "Resolve", :method => :put, :confirm => 'Seriously?', :class => 'resolve' if err.unresolved? - if errs.none? %tr %td{:colspan => (@app ? 5 : 6)} diff --git a/public/images/thumbs-up.png b/public/images/thumbs-up.png new file mode 100644 index 0000000000000000000000000000000000000000..03dad8404c92e45173f0cc3586598cfbc284db87 GIT binary patch literal 1454 zcmV;f1yTBmP)UM-Yc=biBuFGZlcwzs!`#{G{00Rbf}f4-+D8^8`ytsZOuF|Fz^$@nnFTCC@3h10s{jd z1z%oX(#6FEot>S*fVP^Nnjb6{%K=ZjehN5fdV1Pwx7&YZU{PpjC<$0R<}iN%=1{vm z7Vyc*3Hk|}&Gt=xe!hdJUOcwP{##pH*4f$FHw?6ehli8NWO9T3{r#1<0B>$?((&=J zGA21WS$TbZeN7(QQ3PFaadEHW&UC<4NCpe4~s>EP0OJ#*dDU)M29Qf^IOt(mjsi!BBw2JLv%`DJh}7y*=va=%9v% z2DN9X*~P_0nxCJig@pwwFE4lJ0&*ZFCFKKM5HO^y1Z?tpix}TeiH?q@+S*!`EC?sU z3}fJJUS1wWMn) zOgT9@D$Ibw7+n_x46t6ci2}=-SY^dZy=v`N$iV$GX<+`T{46t;tgZF5L7v`RypPQxV5jrFl4hD5~u`;%rIy*bn z-bvO2+}+)+=3~67s!Bxz(ZcqyP8f9n23Vfg;#)q2Du;)M&HB}bQo`|wkjfZ5K^WDA z?mAeI4`^5$6&0nT1n(gLb3_1q`_O&-S3*Ld|MuMj|y z{m2RQtn(dogW>H3lEI7r?CtHfvWG#BJp^>HWGx~gFAzXs07|H?uFk*t<807*qo IM6N<$g7ECJFaQ7m literal 0 HcmV?d00001 diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 8f2a77c153..5dbc42b702 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -151,7 +151,13 @@ request.flash[:success].should match(/Great news/) end - it "should redirect do the errs page" do + it "should redirect to the app page" do + put :resolve, :app_id => @err.app.id, :id => @err.id + response.should redirect_to(app_path(@err.app)) + end + + it "should redirect back to errs page" do + request.env["Referer"] = errs_path put :resolve, :app_id => @err.app.id, :id => @err.id response.should redirect_to(errs_path) end From d4d02da8eb9762b2e0f9a0eebd809ed02fd0e4b6 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Fri, 4 Mar 2011 18:14:01 +0300 Subject: [PATCH 039/115] All errors atom feed. --- app/controllers/errs_controller.rb | 9 ++++++++- app/models/user.rb | 2 +- app/views/errs/index.atom.builder | 11 +++++++++++ app/views/errs/index.html.haml | 2 ++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 app/views/errs/index.atom.builder diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index e2644ee8e5..7496028584 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -4,7 +4,14 @@ class ErrsController < ApplicationController def index app_scope = current_user.admin? ? App.all : current_user.apps - @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + respond_to do |format| + format.html do + @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + end + format.atom do + @errs = Err.for_apps(app_scope).unresolved.ordered + end + end end def all diff --git a/app/models/user.rb b/app/models/user.rb index aac2ab7b5f..9a18357c19 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,7 +10,7 @@ class User field :name field :admin, :type => Boolean, :default => false - + after_destroy :destroy_watchers validates_presence_of :name diff --git a/app/views/errs/index.atom.builder b/app/views/errs/index.atom.builder new file mode 100644 index 0000000000..256d5d51c6 --- /dev/null +++ b/app/views/errs/index.atom.builder @@ -0,0 +1,11 @@ +atom_feed do |feed| + feed.title("Errbit notices at #{root_url}") + feed.updated(@errs.first.created_at) + + for err in @errs + feed.entry(err, :url => app_err_url(err.app, err)) do |entry| + entry.title "[#{ err.environment }] #{ err.app.name } at \"#{ err.where }\"" + entry.summary(err.notices.first.try(:message)) + end + end +end diff --git a/app/views/errs/index.html.haml b/app/views/errs/index.html.haml index 40b02480cb..75315bdb4c 100644 --- a/app/views/errs/index.html.haml +++ b/app/views/errs/index.html.haml @@ -1,4 +1,6 @@ - content_for :title, 'Unresolved Errs' +- content_for :head do + = auto_discovery_link_tag :atom, errs_url(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{root_url}" - content_for :action_bar do = link_to 'show resolved', all_errs_path, :class => 'button' = render 'table', :errs => @errs \ No newline at end of file From 7c74f7107e565af9d69e097675c943720fca3098 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Fri, 4 Mar 2011 18:25:08 +0300 Subject: [PATCH 040/115] Filling auth tokens on user save. --- app/models/user.rb | 1 + spec/controllers/users_controller_spec.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 9a18357c19..b1246ea658 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,6 +12,7 @@ class User field :admin, :type => Boolean, :default => false after_destroy :destroy_watchers + before_save :ensure_authentication_token validates_presence_of :name diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index de8eabecc9..869611a839 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -124,6 +124,11 @@ response.should be_redirect User.find(assigns(:user).to_param).admin.should be_true end + + it "should has auth token" do + post :create, @attrs + User.last.authentication_token.should_not be_blank + end end context "when the create is unsuccessful" do From 98db4dcfdbaa114fe1f3764291842708e5b63824 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Sat, 5 Mar 2011 18:04:39 +0300 Subject: [PATCH 041/115] More information for feed entries. --- app/helpers/notices_helper.rb | 6 ++++ app/views/errs/index.atom.builder | 11 +++++-- app/views/notices/_atom_entry.html.haml | 41 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app/helpers/notices_helper.rb create mode 100644 app/views/notices/_atom_entry.html.haml diff --git a/app/helpers/notices_helper.rb b/app/helpers/notices_helper.rb new file mode 100644 index 0000000000..bd4974b183 --- /dev/null +++ b/app/helpers/notices_helper.rb @@ -0,0 +1,6 @@ +# encoding: utf-8 +module NoticesHelper + def notice_atom_summary notice + render :partial => "notices/atom_entry.html.haml", :locals => {:notice => notice} + end +end \ No newline at end of file diff --git a/app/views/errs/index.atom.builder b/app/views/errs/index.atom.builder index 256d5d51c6..1183f668d7 100644 --- a/app/views/errs/index.atom.builder +++ b/app/views/errs/index.atom.builder @@ -3,9 +3,16 @@ atom_feed do |feed| feed.updated(@errs.first.created_at) for err in @errs + notice = err.notices.first + feed.entry(err, :url => app_err_url(err.app, err)) do |entry| - entry.title "[#{ err.environment }] #{ err.app.name } at \"#{ err.where }\"" - entry.summary(err.notices.first.try(:message)) + entry.title "[#{ err.where }] #{err.message.to_s.truncate(27)}" + entry.author do |author| + author.name "#{ err.app.name } [#{ err.environment }]" + end + if notice + entry.summary(notice_atom_summary(notice), :type => "html") + end end end end diff --git a/app/views/notices/_atom_entry.html.haml b/app/views/notices/_atom_entry.html.haml new file mode 100644 index 0000000000..7396862628 --- /dev/null +++ b/app/views/notices/_atom_entry.html.haml @@ -0,0 +1,41 @@ +%h2= notice.message +%h3 Summary +- if notice.request['url'].present? + %p + %strong URL: + = link_to(notice.request['url'], notice.request['url']) +%p + %strong Where: + = notice.err.where +%p + %strong Occured: + = notice.created_at.to_s(:micro) +%p + %strong Similar: + = notice.err.notices.count - 1 + +%h3 Params +%p= pretty_hash(notice.params) + +%h3 Session +%p= pretty_hash(notice.session) + +%h3 Backtrace +%table + - for line in notice.backtrace + %tr + %td + = "#{line['number']}:" +    + %td + = raw "#{h line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> #{content_tag :strong, h(line['method'])}" + +%h3 Environment +%table + - for key, val in notice.env_vars + %tr + %td + = h key + %td + = h val + From e0e6ee0c071efcd1e02bb33b45184f79be4a63c9 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Sat, 5 Mar 2011 18:29:00 +0300 Subject: [PATCH 042/115] Atom feeds per app. --- app/controllers/apps_controller.rb | 11 +++++++++-- app/views/apps/show.atom.builder | 4 ++++ app/views/apps/show.html.haml | 2 ++ app/views/errs/_list.atom.builder | 15 +++++++++++++++ app/views/errs/index.atom.builder | 16 +--------------- 5 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 app/views/apps/show.atom.builder create mode 100644 app/views/errs/_list.atom.builder diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 84533debf0..31f7d80051 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -8,8 +8,15 @@ def index end def show - @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) - @deploys = @app.deploys.order_by(:created_at.desc).limit(5) + respond_to do |format| + format.html do + @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + @deploys = @app.deploys.order_by(:created_at.desc).limit(5) + end + format.atom do + @errs = @app.errs.unresolved.ordered + end + end end def new diff --git a/app/views/apps/show.atom.builder b/app/views/apps/show.atom.builder new file mode 100644 index 0000000000..424a97b89c --- /dev/null +++ b/app/views/apps/show.atom.builder @@ -0,0 +1,4 @@ +atom_feed do |feed| + feed.title("Errbit notices for #{h @app.name} at #{root_url}") + render :partial => "errs/list", :locals => {:feed => feed} +end diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml index 5c70b10c39..593b14fd19 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -1,4 +1,6 @@ - content_for :title, @app.name +- content_for :head do + = auto_discovery_link_tag :atom, app_url(@app, User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices for #{@app.name} at #{root_url}" - content_for :meta do %strong Errs Caught: = @app.errs.count diff --git a/app/views/errs/_list.atom.builder b/app/views/errs/_list.atom.builder new file mode 100644 index 0000000000..f9d8ef06e5 --- /dev/null +++ b/app/views/errs/_list.atom.builder @@ -0,0 +1,15 @@ +feed.updated(@errs.first.created_at) + +for err in @errs + notice = err.notices.first + + feed.entry(err, :url => app_err_url(err.app, err)) do |entry| + entry.title "[#{ err.where }] #{err.message.to_s.truncate(27)}" + entry.author do |author| + author.name "#{ err.app.name } [#{ err.environment }]" + end + if notice + entry.summary(notice_atom_summary(notice), :type => "html") + end + end +end diff --git a/app/views/errs/index.atom.builder b/app/views/errs/index.atom.builder index 1183f668d7..b0a77d5d5c 100644 --- a/app/views/errs/index.atom.builder +++ b/app/views/errs/index.atom.builder @@ -1,18 +1,4 @@ atom_feed do |feed| feed.title("Errbit notices at #{root_url}") - feed.updated(@errs.first.created_at) - - for err in @errs - notice = err.notices.first - - feed.entry(err, :url => app_err_url(err.app, err)) do |entry| - entry.title "[#{ err.where }] #{err.message.to_s.truncate(27)}" - entry.author do |author| - author.name "#{ err.app.name } [#{ err.environment }]" - end - if notice - entry.summary(notice_atom_summary(notice), :type => "html") - end - end - end + render :partial => "errs/list", :locals => {:feed => feed} end From 5d63d531c272c1a123179874e75b2786b022cc42 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Sat, 5 Mar 2011 18:47:44 +0300 Subject: [PATCH 043/115] Feeds specs. --- spec/controllers/apps_controller_spec.rb | 8 ++++++++ spec/controllers/errs_controller_spec.rb | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 840e285045..70bf8c6ae5 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -37,6 +37,8 @@ before(:each) do sign_in Factory(:admin) @app = Factory(:app) + @err = Factory :err, :app => @app + @notice = Factory :notice, :err => @err end it 'finds the app' do @@ -48,6 +50,12 @@ Factory :err, :app => @app lambda { get :show, :id => @app.id }.should_not raise_error end + + it "should list atom feed successfully" do + get :show, :id => @app.id, :format => "atom" + response.should be_success + response.body.should match(@err.message) + end end context 'logged in as a user' do diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 5dbc42b702..a37ee183c8 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -11,19 +11,24 @@ let(:err) { Factory(:err, :app => app) } describe "GET /errs" do + render_views context 'when logged in as an admin' do before(:each) do sign_in Factory(:admin) + @notice = Factory :notice + @err = @notice.err end - it "gets a paginated list of unresolved errs" do - errs = WillPaginate::Collection.new(1,30) - 3.times { errs << Factory(:err) } - Err.should_receive(:unresolved).and_return( - mock('proxy', :ordered => mock('proxy', :paginate => errs)) - ) + it "should successfully list errs" do get :index - assigns(:errs).should == errs + response.should be_success + response.body.should match(@err.message) + end + + it "should list atom feed successfully" do + get :index, :format => "atom" + response.should be_success + response.body.should match(@err.message) end it "should handle lots of errors" do From 14da0870dfb27f81212505890bb3c13a1b2a9642 Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Tue, 8 Mar 2011 15:14:34 +0100 Subject: [PATCH 044/115] Trying to add indexes --- app/models/err.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/models/err.rb b/app/models/err.rb index 496c773cd6..ad69e305b9 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -1,7 +1,7 @@ class Err include Mongoid::Document include Mongoid::Timestamps - + field :klass field :component field :action @@ -9,39 +9,42 @@ class Err field :fingerprint field :last_notice_at, :type => DateTime field :resolved, :type => Boolean, :default => false - + referenced_in :app embeds_many :notices - + validates_presence_of :klass, :environment - + scope :resolved, where(:resolved => true) scope :unresolved, where(:resolved => false) scope :ordered, order_by(:last_notice_at.desc) scope :in_env, lambda {|env| where(:environment => env)} scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} - + + index :last_notice_at + index :resolved + def self.for(attrs) app = attrs.delete(:app) app.errs.where(attrs).first || app.errs.create!(attrs) end - + def resolve! self.update_attributes!(:resolved => true) end - + def unresolved? !resolved? end - + def where where = component.dup where << "##{action}" if action.present? where end - + def message notices.first.message || klass end - + end \ No newline at end of file From e0ff252a182dc75a6ef7171a3c2636af8590f7b2 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 9 Mar 2011 16:07:14 +0300 Subject: [PATCH 045/115] Per-user per_page option for errs and users entries. --- app/controllers/apps_controller.rb | 2 +- app/controllers/errs_controller.rb | 4 ++-- app/controllers/users_controller.rb | 2 +- app/models/err.rb | 2 -- app/models/user.rb | 8 ++++++-- spec/controllers/apps_controller_spec.rb | 20 +++++++++++++++++++- spec/controllers/errs_controller_spec.rb | 20 +++++++++++++++++++- 7 files changed, 48 insertions(+), 10 deletions(-) diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 31f7d80051..7b32f3b13f 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -10,7 +10,7 @@ def index def show respond_to do |format| format.html do - @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + @errs = @app.errs.ordered.paginate(:page => params[:page], :per_page => current_user.per_page) @deploys = @app.deploys.order_by(:created_at.desc).limit(5) end format.atom do diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index 7496028584..e7ca172395 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -6,7 +6,7 @@ def index app_scope = current_user.admin? ? App.all : current_user.apps respond_to do |format| format.html do - @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page], :per_page => Err.per_page) + @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page], :per_page => current_user.per_page) end format.atom do @errs = Err.for_apps(app_scope).unresolved.ordered @@ -16,7 +16,7 @@ def index def all app_scope = current_user.admin? ? App.all : current_user.apps - @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => Err.per_page) + @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) end def show diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 932f46d9d4..1165185f8d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -6,7 +6,7 @@ class UsersController < ApplicationController before_filter :require_user_edit_priviledges, :only => [:edit, :update] def index - @users = User.paginate(:page => params[:page], :per_page => User.per_page) + @users = User.paginate(:page => params[:page], :per_page => current_user.per_page) end def show diff --git a/app/models/err.rb b/app/models/err.rb index a43676838a..d38f1ebeb7 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -1,6 +1,4 @@ class Err - cattr_reader :per_page - @@per_page = 30 include Mongoid::Document include Mongoid::Timestamps diff --git a/app/models/user.rb b/app/models/user.rb index b1246ea658..61146f6c22 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,5 @@ class User - cattr_reader :per_page - @@per_page = 30 + PER_PAGE = 30 include Mongoid::Document include Mongoid::Timestamps @@ -10,6 +9,7 @@ class User field :name field :admin, :type => Boolean, :default => false + field :per_page, :type => Fixnum, :default => PER_PAGE after_destroy :destroy_watchers before_save :ensure_authentication_token @@ -23,6 +23,10 @@ class User def watchers App.all.map(&:watchers).flatten.select {|w| w.user_id.to_s == id.to_s} end + + def per_page + self[:per_page] || PER_PAGE + end def apps # This is completely wasteful but became necessary diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 70bf8c6ae5..7eb420a74f 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -35,7 +35,8 @@ render_views context 'logged in as an admin' do before(:each) do - sign_in Factory(:admin) + @user = Factory(:admin) + sign_in @user @app = Factory(:app) @err = Factory :err, :app => @app @notice = Factory :notice, :err => @err @@ -56,6 +57,23 @@ response.should be_success response.body.should match(@err.message) end + + context "pagination" do + before(:each) do + 35.times { Factory :err, :app => @app } + end + + it "should have default per_page value for user" do + get :show, :id => @app.id + assigns(:errs).size.should == User::PER_PAGE + end + + it "should be able to override default per_page value" do + @user.update_attribute :per_page, 10 + get :show, :id => @app.id + assigns(:errs).size.should == 10 + end + end end context 'logged in as a user' do diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index a37ee183c8..ad2bbf2694 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -14,7 +14,8 @@ render_views context 'when logged in as an admin' do before(:each) do - sign_in Factory(:admin) + @user = Factory(:admin) + sign_in @user @notice = Factory :notice @err = @notice.err end @@ -35,6 +36,23 @@ 1000.times { Factory :notice } lambda { get :index }.should_not raise_error end + + context "pagination" do + before(:each) do + 35.times { Factory :err } + end + + it "should have default per_page value for user" do + get :index + assigns(:errs).size.should == User::PER_PAGE + end + + it "should be able to override default per_page value" do + @user.update_attribute :per_page, 10 + get :index + assigns(:errs).size.should == 10 + end + end end context 'when logged in as a user' do From 6191e44e3ee4dfef6a78c5dfe9cf96d41f6e04e1 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 9 Mar 2011 16:16:10 +0300 Subject: [PATCH 046/115] per_page field at user profile page --- app/views/users/_fields.html.haml | 4 ++++ spec/controllers/users_controller_spec.rb | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/app/views/users/_fields.html.haml b/app/views/users/_fields.html.haml index f116059bf2..b6c6b47747 100644 --- a/app/views/users/_fields.html.haml +++ b/app/views/users/_fields.html.haml @@ -7,6 +7,10 @@ .required = f.label :email = f.text_field :email + +.required + = f.label 'Entries per page' + = f.select :per_page, [10, 20, 30, 50, 75, 100] .required = f.label :password diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 869611a839..50b52f1c05 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe UsersController do + render_views it_requires_authentication it_requires_admin_privileges :for => { @@ -28,6 +29,11 @@ get :edit, :id => @user.id assigns(:user).should == @user end + + it "should have per_page option" do + get :edit, :id => @user.id + response.body.should match(/id="user_per_page"/) + end end context "PUT /users/:other_id" do @@ -53,6 +59,11 @@ put :update, :id => @user.to_param, :user => {:admin => true} @user.reload.admin.should be_false end + + it "should be able to set per_page option" do + put :update, :id => @user.to_param, :user => {:per_page => 555} + @user.reload.per_page.should == 555 + end end context "when the update is unsuccessful" do From e6e45763b8de063ced7fb08118f3440e2e2d6453 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 9 Mar 2011 16:21:22 +0300 Subject: [PATCH 047/115] User pagination spec fix. --- spec/controllers/users_controller_spec.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 50b52f1c05..68da52f7b9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -77,15 +77,16 @@ context 'Signed in as an admin' do before do - sign_in Factory(:admin) + @user = Factory(:admin) + sign_in @user end context "GET /users" do it 'paginates all users' do - users = 3.times.inject(WillPaginate::Collection.new(1,30)) {|page,_| page << Factory.build(:user)} - User.should_receive(:paginate).and_return(users) + @user.update_attribute :per_page, 2 + users = 3.times { Factory(:user) } get :index - assigns(:users).should == users + assigns(:users).size.should == 2 end end From 4bf7dbacfdc80515faa516d393d6a9319e659e3c Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 9 Mar 2011 16:23:27 +0300 Subject: [PATCH 048/115] Turn off autocomplete for user profile form. --- app/views/users/edit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml index 93b75cbf54..c0eb807de9 100644 --- a/app/views/users/edit.html.haml +++ b/app/views/users/edit.html.haml @@ -1,7 +1,7 @@ - content_for :title, "Edit #{@user.name}" - content_for :action_bar, link_to('cancel', user_path(@user), :class => 'button') -= form_for @user do |f| += form_for @user, :html => {:autocomplete => "off"} do |f| = @user.errors.full_messages.to_sentence = render 'fields', :f => f From 918811f1be87e4aa53810c7e4329368c44d59447 Mon Sep 17 00:00:00 2001 From: Joshua Priddle Date: Wed, 9 Mar 2011 22:53:26 -0500 Subject: [PATCH 049/115] Add config.secure to install instructions --- app/views/apps/_configuration_instructions.html.haml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/apps/_configuration_instructions.html.haml b/app/views/apps/_configuration_instructions.html.haml index 62abcf4872..c951c4366c 100644 --- a/app/views/apps/_configuration_instructions.html.haml +++ b/app/views/apps/_configuration_instructions.html.haml @@ -1,7 +1,7 @@ %pre %code :preserve - + # Require the hoptoad_notifier gem in you App. # -------------------------------------------- # @@ -14,9 +14,10 @@ # Then add the following to config/initializers/errbit.rb # ------------------------------------------------------- HoptoadNotifier.configure do |config| - config.api_key = '#{app.api_key}' - config.host = '#{request.host}' - config.port = #{request.port} + config.api_key = '#{app.api_key}' + config.host = '#{request.host}' + config.port = #{request.port} + config.secure = config.port == 443 end # # Testing @@ -28,5 +29,4 @@ # Run: # rake hoptoad:test # refresh this page - - \ No newline at end of file + From 96ede760d78ce2d7daf8e83403242b11c0175155 Mon Sep 17 00:00:00 2001 From: Akzhan Abdulin Date: Thu, 10 Mar 2011 15:47:05 +0300 Subject: [PATCH 050/115] Upgrade jQuery from 1.4.2 to 1.5.1 (some updates for modern and upcoming browsers) --- public/javascripts/jquery.js | 148 ++--------------------------------- 1 file changed, 5 insertions(+), 143 deletions(-) diff --git a/public/javascripts/jquery.js b/public/javascripts/jquery.js index 48a88b8f48..6437874c69 100644 --- a/public/javascripts/jquery.js +++ b/public/javascripts/jquery.js @@ -1,154 +1,16 @@ /*! - * jQuery JavaScript Library v1.4.2 + * jQuery JavaScript Library v1.5.1 * http://jquery.com/ * - * Copyright 2010, John Resig + * Copyright 2011, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ - * Copyright 2010, The Dojo Foundation + * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * - * Date: Sat Feb 13 22:33:48 2010 -0500 + * Date: Wed Feb 23 13:55:29 2011 -0500 */ -(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, -Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& -(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, -a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== -"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, -function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
a"; -var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, -parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= -false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= -s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, -applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; -else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, -a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== -w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, -cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= -c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); -a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, -function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); -k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), -C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= -e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& -f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; -if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", -e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, -"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, -d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, -e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); -t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| -g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, -CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, -g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, -text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, -setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= -h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== -"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, -h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& -q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; -if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); -(function(){var g=s.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: -function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= -{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== -"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", -d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? -a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== -1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= -c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, -wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, -prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, -this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); -return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, -""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); -return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", -""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= -c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? -c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= -function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= -Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, -"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= -a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= -a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== -"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, -serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), -function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, -global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& -e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? -"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== -false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= -false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", -c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| -d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); -g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== -1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== -"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; -if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== -"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| -c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; -this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= -this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, -e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
"; -a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); -c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, -d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- -f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": -"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in -e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); \ No newline at end of file +(function(a,b){function cg(a){return d.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cd(a){if(!bZ[a]){var b=d("<"+a+">").appendTo("body"),c=b.css("display");b.remove();if(c==="none"||c==="")c="block";bZ[a]=c}return bZ[a]}function cc(a,b){var c={};d.each(cb.concat.apply([],cb.slice(0,b)),function(){c[this]=a});return c}function bY(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function bX(){try{return new a.XMLHttpRequest}catch(b){}}function bW(){d(a).unload(function(){for(var a in bU)bU[a](0,1)})}function bQ(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var e=a.dataTypes,f={},g,h,i=e.length,j,k=e[0],l,m,n,o,p;for(g=1;g=0===c})}function N(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function F(a,b){return(a&&a!=="*"?a+".":"")+b.replace(r,"`").replace(s,"&")}function E(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,q=[],r=[],s=d._data(this,"events");if(a.liveFired!==this&&s&&s.live&&!a.target.disabled&&(!a.button||a.type!=="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var t=s.live.slice(0);for(i=0;ic)break;a.currentTarget=f.elem,a.data=f.handleObj.data,a.handleObj=f.handleObj,o=f.handleObj.origHandler.apply(f.elem,arguments);if(o===!1||a.isPropagationStopped()){c=f.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function C(a,c,e){var f=d.extend({},e[0]);f.type=a,f.originalEvent={},f.liveFired=b,d.event.handle.call(c,f),f.isDefaultPrevented()&&e[0].preventDefault()}function w(){return!0}function v(){return!1}function g(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function f(a,c,f){if(f===b&&a.nodeType===1){f=a.getAttribute("data-"+c);if(typeof f==="string"){try{f=f==="true"?!0:f==="false"?!1:f==="null"?null:d.isNaN(f)?e.test(f)?d.parseJSON(f):f:parseFloat(f)}catch(g){}d.data(a,c,f)}else f=b}return f}var c=a.document,d=function(){function I(){if(!d.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(I,1);return}d.ready()}}var d=function(a,b){return new d.fn.init(a,b,g)},e=a.jQuery,f=a.$,g,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,i=/\S/,j=/^\s+/,k=/\s+$/,l=/\d/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=navigator.userAgent,w,x=!1,y,z="then done fail isResolved isRejected promise".split(" "),A,B=Object.prototype.toString,C=Object.prototype.hasOwnProperty,D=Array.prototype.push,E=Array.prototype.slice,F=String.prototype.trim,G=Array.prototype.indexOf,H={};d.fn=d.prototype={constructor:d,init:function(a,e,f){var g,i,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!e&&c.body){this.context=c,this[0]=c.body,this.selector="body",this.length=1;return this}if(typeof a==="string"){g=h.exec(a);if(!g||!g[1]&&e)return!e||e.jquery?(e||f).find(a):this.constructor(e).find(a);if(g[1]){e=e instanceof d?e[0]:e,k=e?e.ownerDocument||e:c,j=m.exec(a),j?d.isPlainObject(e)?(a=[c.createElement(j[1])],d.fn.attr.call(a,e,!0)):a=[k.createElement(j[1])]:(j=d.buildFragment([g[1]],[k]),a=(j.cacheable?d.clone(j.fragment):j.fragment).childNodes);return d.merge(this,a)}i=c.getElementById(g[2]);if(i&&i.parentNode){if(i.id!==g[2])return f.find(a);this.length=1,this[0]=i}this.context=c,this.selector=a;return this}if(d.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return d.makeArray(a,this)},selector:"",jquery:"1.5.1",length:0,size:function(){return this.length},toArray:function(){return E.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var e=this.constructor();d.isArray(a)?D.apply(e,a):d.merge(e,a),e.prevObject=this,e.context=this.context,b==="find"?e.selector=this.selector+(this.selector?" ":"")+c:b&&(e.selector=this.selector+"."+b+"("+c+")");return e},each:function(a,b){return d.each(this,a,b)},ready:function(a){d.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(E.apply(this,arguments),"slice",E.call(arguments).join(","))},map:function(a){return this.pushStack(d.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:D,sort:[].sort,splice:[].splice},d.fn.init.prototype=d.fn,d.extend=d.fn.extend=function(){var a,c,e,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i==="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!=="object"&&!d.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;y.resolveWith(c,[d]),d.fn.trigger&&d(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!x){x=!0;if(c.readyState==="complete")return setTimeout(d.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",A,!1),a.addEventListener("load",d.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",A),a.attachEvent("onload",d.ready);var b=!1;try{b=a.frameElement==null}catch(e){}c.documentElement.doScroll&&b&&I()}}},isFunction:function(a){return d.type(a)==="function"},isArray:Array.isArray||function(a){return d.type(a)==="array"},isWindow:function(a){return a&&typeof a==="object"&&"setInterval"in a},isNaN:function(a){return a==null||!l.test(a)||isNaN(a)},type:function(a){return a==null?String(a):H[B.call(a)]||"object"},isPlainObject:function(a){if(!a||d.type(a)!=="object"||a.nodeType||d.isWindow(a))return!1;if(a.constructor&&!C.call(a,"constructor")&&!C.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a){}return c===b||C.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!=="string"||!b)return null;b=d.trim(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return a.JSON&&a.JSON.parse?a.JSON.parse(b):(new Function("return "+b))();d.error("Invalid JSON: "+b)},parseXML:function(b,c,e){a.DOMParser?(e=new DOMParser,c=e.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),e=c.documentElement,(!e||!e.nodeName||e.nodeName==="parsererror")&&d.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(a){if(a&&i.test(a)){var b=c.head||c.getElementsByTagName("head")[0]||c.documentElement,e=c.createElement("script");d.support.scriptEval()?e.appendChild(c.createTextNode(a)):e.text=a,b.insertBefore(e,b.firstChild),b.removeChild(e)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,e){var f,g=0,h=a.length,i=h===b||d.isFunction(a);if(e){if(i){for(f in a)if(c.apply(a[f],e)===!1)break}else for(;g1){var f=E.call(arguments,0),g=b,h=function(a){return function(b){f[a]=arguments.length>1?E.call(arguments,0):b,--g||c.resolveWith(e,f)}};while(b--)a=f[b],a&&d.isFunction(a.promise)?a.promise().then(h(b),c.reject):--g;g||c.resolveWith(e,f)}else c!==a&&c.resolve(a);return e},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}d.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.subclass=this.subclass,a.fn.init=function b(b,c){c&&c instanceof d&&!(c instanceof a)&&(c=a(c));return d.fn.init.call(this,b,c,e)},a.fn.init.prototype=a.fn;var e=a(c);return a},browser:{}}),y=d._Deferred(),d.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){H["[object "+b+"]"]=b.toLowerCase()}),w=d.uaMatch(v),w.browser&&(d.browser[w.browser]=!0,d.browser.version=w.version),d.browser.webkit&&(d.browser.safari=!0),G&&(d.inArray=function(a,b){return G.call(b,a)}),i.test(" ")&&(j=/^[\s\xA0]+/,k=/[\s\xA0]+$/),g=d(c),c.addEventListener?A=function(){c.removeEventListener("DOMContentLoaded",A,!1),d.ready()}:c.attachEvent&&(A=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",A),d.ready())});return d}();(function(){d.support={};var b=c.createElement("div");b.style.display="none",b.innerHTML="
a";var e=b.getElementsByTagName("*"),f=b.getElementsByTagName("a")[0],g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=b.getElementsByTagName("input")[0];if(e&&e.length&&f){d.support={leadingWhitespace:b.firstChild.nodeType===3,tbody:!b.getElementsByTagName("tbody").length,htmlSerialize:!!b.getElementsByTagName("link").length,style:/red/.test(f.getAttribute("style")),hrefNormalized:f.getAttribute("href")==="/a",opacity:/^0.55$/.test(f.style.opacity),cssFloat:!!f.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,deleteExpando:!0,optDisabled:!1,checkClone:!1,noCloneEvent:!0,noCloneChecked:!0,boxModel:null,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableHiddenOffsets:!0},i.checked=!0,d.support.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,d.support.optDisabled=!h.disabled;var j=null;d.support.scriptEval=function(){if(j===null){var b=c.documentElement,e=c.createElement("script"),f="script"+d.now();try{e.appendChild(c.createTextNode("window."+f+"=1;"))}catch(g){}b.insertBefore(e,b.firstChild),a[f]?(j=!0,delete a[f]):j=!1,b.removeChild(e),b=e=f=null}return j};try{delete b.test}catch(k){d.support.deleteExpando=!1}!b.addEventListener&&b.attachEvent&&b.fireEvent&&(b.attachEvent("onclick",function l(){d.support.noCloneEvent=!1,b.detachEvent("onclick",l)}),b.cloneNode(!0).fireEvent("onclick")),b=c.createElement("div"),b.innerHTML="";var m=c.createDocumentFragment();m.appendChild(b.firstChild),d.support.checkClone=m.cloneNode(!0).cloneNode(!0).lastChild.checked,d(function(){var a=c.createElement("div"),b=c.getElementsByTagName("body")[0];if(b){a.style.width=a.style.paddingLeft="1px",b.appendChild(a),d.boxModel=d.support.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,d.support.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
";var e=a.getElementsByTagName("td");d.support.reliableHiddenOffsets=e[0].offsetHeight===0,e[0].style.display="",e[1].style.display="none",d.support.reliableHiddenOffsets=d.support.reliableHiddenOffsets&&e[0].offsetHeight===0,a.innerHTML="",b.removeChild(a).style.display="none",a=e=null}});var n=function(a){var b=c.createElement("div");a="on"+a;if(!b.attachEvent)return!0;var d=a in b;d||(b.setAttribute(a,"return;"),d=typeof b[a]==="function"),b=null;return d};d.support.submitBubbles=n("submit"),d.support.changeBubbles=n("change"),b=e=f=null}})();var e=/^(?:\{.*\}|\[.*\])$/;d.extend({cache:{},uuid:0,expando:"jQuery"+(d.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?d.cache[a[d.expando]]:a[d.expando];return!!a&&!g(a)},data:function(a,c,e,f){if(d.acceptData(a)){var g=d.expando,h=typeof c==="string",i,j=a.nodeType,k=j?d.cache:a,l=j?a[d.expando]:a[d.expando]&&d.expando;if((!l||f&&l&&!k[l][g])&&h&&e===b)return;l||(j?a[d.expando]=l=++d.uuid:l=d.expando),k[l]||(k[l]={},j||(k[l].toJSON=d.noop));if(typeof c==="object"||typeof c==="function")f?k[l][g]=d.extend(k[l][g],c):k[l]=d.extend(k[l],c);i=k[l],f&&(i[g]||(i[g]={}),i=i[g]),e!==b&&(i[c]=e);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,e){if(d.acceptData(b)){var f=d.expando,h=b.nodeType,i=h?d.cache:b,j=h?b[d.expando]:d.expando;if(!i[j])return;if(c){var k=e?i[j][f]:i[j];if(k){delete k[c];if(!g(k))return}}if(e){delete i[j][f];if(!g(i[j]))return}var l=i[j][f];d.support.deleteExpando||i!=a?delete i[j]:i[j]=null,l?(i[j]={},h||(i[j].toJSON=d.noop),i[j][f]=l):h&&(d.support.deleteExpando?delete b[d.expando]:b.removeAttribute?b.removeAttribute(d.expando):b[d.expando]=null)}},_data:function(a,b,c){return d.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=d.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),d.fn.extend({data:function(a,c){var e=null;if(typeof a==="undefined"){if(this.length){e=d.data(this[0]);if(this[0].nodeType===1){var g=this[0].attributes,h;for(var i=0,j=g.length;i-1)return!0;return!1},val:function(a){if(!arguments.length){var c=this[0];if(c){if(d.nodeName(c,"option")){var e=c.attributes.value;return!e||e.specified?c.value:c.text}if(d.nodeName(c,"select")){var f=c.selectedIndex,g=[],h=c.options,i=c.type==="select-one";if(f<0)return null;for(var k=i?f:0,l=i?f+1:h.length;k=0;else if(d.nodeName(this,"select")){var f=d.makeArray(e);d("option",this).each(function(){this.selected=d.inArray(d(this).val(),f)>=0}),f.length||(this.selectedIndex=-1)}else this.value=e}})}}),d.extend({attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,e,f){if(!a||a.nodeType===3||a.nodeType===8||a.nodeType===2)return b;if(f&&c in d.attrFn)return d(a)[c](e);var g=a.nodeType!==1||!d.isXMLDoc(a),h=e!==b;c=g&&d.props[c]||c;if(a.nodeType===1){var i=k.test(c);if(c==="selected"&&!d.support.optSelected){var j=a.parentNode;j&&(j.selectedIndex,j.parentNode&&j.parentNode.selectedIndex)}if((c in a||a[c]!==b)&&g&&!i){h&&(c==="type"&&l.test(a.nodeName)&&a.parentNode&&d.error("type property can't be changed"),e===null?a.nodeType===1&&a.removeAttribute(c):a[c]=e);if(d.nodeName(a,"form")&&a.getAttributeNode(c))return a.getAttributeNode(c).nodeValue;if(c==="tabIndex"){var o=a.getAttributeNode("tabIndex");return o&&o.specified?o.value:m.test(a.nodeName)||n.test(a.nodeName)&&a.href?0:b}return a[c]}if(!d.support.style&&g&&c==="style"){h&&(a.style.cssText=""+e);return a.style.cssText}h&&a.setAttribute(c,""+e);if(!a.attributes[c]&&(a.hasAttribute&&!a.hasAttribute(c)))return b;var p=!d.support.hrefNormalized&&g&&i?a.getAttribute(c,2):a.getAttribute(c);return p===null?b:p}h&&(a[c]=e);return a[c]}});var p=/\.(.*)$/,q=/^(?:textarea|input|select)$/i,r=/\./g,s=/ /g,t=/[^\w\s.|`]/g,u=function(a){return a.replace(t,"\\$&")};d.event={add:function(c,e,f,g){if(c.nodeType!==3&&c.nodeType!==8){try{d.isWindow(c)&&(c!==a&&!c.frameElement)&&(c=a)}catch(h){}if(f===!1)f=v;else if(!f)return;var i,j;f.handler&&(i=f,f=i.handler),f.guid||(f.guid=d.guid++);var k=d._data(c);if(!k)return;var l=k.events,m=k.handle;l||(k.events=l={}),m||(k.handle=m=function(){return typeof d!=="undefined"&&!d.event.triggered?d.event.handle.apply(m.elem,arguments):b}),m.elem=c,e=e.split(" ");var n,o=0,p;while(n=e[o++]){j=i?d.extend({},i):{handler:f,data:g},n.indexOf(".")>-1?(p=n.split("."),n=p.shift(),j.namespace=p.slice(0).sort().join(".")):(p=[],j.namespace=""),j.type=n,j.guid||(j.guid=f.guid);var q=l[n],r=d.event.special[n]||{};if(!q){q=l[n]=[];if(!r.setup||r.setup.call(c,g,p,m)===!1)c.addEventListener?c.addEventListener(n,m,!1):c.attachEvent&&c.attachEvent("on"+n,m)}r.add&&(r.add.call(c,j),j.handler.guid||(j.handler.guid=f.guid)),q.push(j),d.event.global[n]=!0}c=null}},global:{},remove:function(a,c,e,f){if(a.nodeType!==3&&a.nodeType!==8){e===!1&&(e=v);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=d.hasData(a)&&d._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(e=c.handler,c=c.type);if(!c||typeof c==="string"&&c.charAt(0)==="."){c=c||"";for(h in t)d.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+d.map(m.slice(0).sort(),u).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!e){for(j=0;j=0&&(a.type=f=f.slice(0,-1),a.exclusive=!0),e||(a.stopPropagation(),d.event.global[f]&&d.each(d.cache,function(){var b=d.expando,e=this[b];e&&e.events&&e.events[f]&&d.event.trigger(a,c,e.handle.elem)}));if(!e||e.nodeType===3||e.nodeType===8)return b;a.result=b,a.target=e,c=d.makeArray(c),c.unshift(a)}a.currentTarget=e;var h=d._data(e,"handle");h&&h.apply(e,c);var i=e.parentNode||e.ownerDocument;try{e&&e.nodeName&&d.noData[e.nodeName.toLowerCase()]||e["on"+f]&&e["on"+f].apply(e,c)===!1&&(a.result=!1,a.preventDefault())}catch(j){}if(!a.isPropagationStopped()&&i)d.event.trigger(a,c,i,!0);else if(!a.isDefaultPrevented()){var k,l=a.target,m=f.replace(p,""),n=d.nodeName(l,"a")&&m==="click",o=d.event.special[m]||{};if((!o._default||o._default.call(e,a)===!1)&&!n&&!(l&&l.nodeName&&d.noData[l.nodeName.toLowerCase()])){try{l[m]&&(k=l["on"+m],k&&(l["on"+m]=null),d.event.triggered=!0,l[m]())}catch(q){}k&&(l["on"+m]=k),d.event.triggered=!1}}},handle:function(c){var e,f,g,h,i,j=[],k=d.makeArray(arguments);c=k[0]=d.event.fix(c||a.event),c.currentTarget=this,e=c.type.indexOf(".")<0&&!c.exclusive,e||(g=c.type.split("."),c.type=g.shift(),j=g.slice(0).sort(),h=new RegExp("(^|\\.)"+j.join("\\.(?:.*\\.)?")+"(\\.|$)")),c.namespace=c.namespace||j.join("."),i=d._data(this,"events"),f=(i||{})[c.type];if(i&&f){f=f.slice(0);for(var l=0,m=f.length;l-1?d.map(a.options,function(a){return a.selected}).join("-"):"":a.nodeName.toLowerCase()==="select"&&(c=a.selectedIndex);return c},B=function B(a){var c=a.target,e,f;if(q.test(c.nodeName)&&!c.readOnly){e=d._data(c,"_change_data"),f=A(c),(a.type!=="focusout"||c.type!=="radio")&&d._data(c,"_change_data",f);if(e===b||f===e)return;if(e!=null||f)a.type="change",a.liveFired=b,d.event.trigger(a,arguments[1],c)}};d.event.special.change={filters:{focusout:B,beforedeactivate:B,click:function(a){var b=a.target,c=b.type;(c==="radio"||c==="checkbox"||b.nodeName.toLowerCase()==="select")&&B.call(this,a)},keydown:function(a){var b=a.target,c=b.type;(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&B.call(this,a)},beforeactivate:function(a){var b=a.target;d._data(b,"_change_data",A(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in z)d.event.add(this,c+".specialChange",z[c]);return q.test(this.nodeName)},teardown:function(a){d.event.remove(this,".specialChange");return q.test(this.nodeName)}},z=d.event.special.change.filters,z.focus=z.beforeactivate}c.addEventListener&&d.each({focus:"focusin",blur:"focusout"},function(a,b){function c(a){a=d.event.fix(a),a.type=b;return d.event.handle.call(this,a)}d.event.special[b]={setup:function(){this.addEventListener(a,c,!0)},teardown:function(){this.removeEventListener(a,c,!0)}}}),d.each(["bind","one"],function(a,c){d.fn[c]=function(a,e,f){if(typeof a==="object"){for(var g in a)this[c](g,e,a[g],f);return this}if(d.isFunction(e)||e===!1)f=e,e=b;var h=c==="one"?d.proxy(f,function(a){d(this).unbind(a,h);return f.apply(this,arguments)}):f;if(a==="unload"&&c!=="one")this.one(a,e,f);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},d.attrFn&&(d.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,e,g){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!=="string")return e;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(f.call(n)==="[object Array]")if(u)if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&e.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&e.push(j[t]);else e.push.apply(e,n);else p(n,e);o&&(k(o,h,e,g),k.uniqueSort(e));return e};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b==="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){return"text"===a.getAttribute("type")},radio:function(a){return"radio"===a.type},checkbox:function(a){return"checkbox"===a.type},file:function(a){return"file"===a.type},password:function(a){return"password"===a.type},submit:function(a){return"submit"===a.type},image:function(a){return"image"===a.type},reset:function(a){return"reset"===a.type},button:function(a){return"button"===a.type||a.nodeName.toLowerCase()==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(f.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length==="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!=="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!=="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!=="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!=="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector,d=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(e){d=!0}b&&(k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(d||!l.match.PSEUDO.test(c)&&!/!=/.test(c))return b.call(a,c)}catch(e){}return k(c,null,null,[a]).length>0})}(),function(){var a=c.createElement("div");a.innerHTML="
";if(a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!=="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(var g=c;g0},closest:function(a,b){var c=[],e,f,g=this[0];if(d.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(e=0,f=a.length;e-1:d(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=L.test(a)?d(a,b||this.context):null;for(e=0,f=this.length;e-1:d.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b)break}}c=c.length>1?d.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a==="string")return d.inArray(this[0],a?d(a):this.parent().children());return d.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a==="string"?d(a,b):d.makeArray(a),e=d.merge(this.get(),c);return this.pushStack(N(c[0])||N(e[0])?e:d.unique(e))},andSelf:function(){return this.add(this.prevObject)}}),d.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return d.dir(a,"parentNode")},parentsUntil:function(a,b,c){return d.dir(a,"parentNode",c)},next:function(a){return d.nth(a,2,"nextSibling")},prev:function(a){return d.nth(a,2,"previousSibling")},nextAll:function(a){return d.dir(a,"nextSibling")},prevAll:function(a){return d.dir(a,"previousSibling")},nextUntil:function(a,b,c){return d.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return d.dir(a,"previousSibling",c)},siblings:function(a){return d.sibling(a.parentNode.firstChild,a)},children:function(a){return d.sibling(a.firstChild)},contents:function(a){return d.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:d.makeArray(a.childNodes)}},function(a,b){d.fn[a]=function(c,e){var f=d.map(this,b,c),g=K.call(arguments);G.test(a)||(e=c),e&&typeof e==="string"&&(f=d.filter(e,f)),f=this.length>1&&!M[a]?d.unique(f):f,(this.length>1||I.test(e))&&H.test(a)&&(f=f.reverse());return this.pushStack(f,a,g.join(","))}}),d.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?d.find.matchesSelector(b[0],a)?[b[0]]:[]:d.find.matches(a,b)},dir:function(a,c,e){var f=[],g=a[c];while(g&&g.nodeType!==9&&(e===b||g.nodeType!==1||!d(g).is(e)))g.nodeType===1&&f.push(g),g=g[c];return f},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var P=/ jQuery\d+="(?:\d+|null)"/g,Q=/^\s+/,R=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,S=/<([\w:]+)/,T=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};X.optgroup=X.option,X.tbody=X.tfoot=X.colgroup=X.caption=X.thead,X.th=X.td,d.support.htmlSerialize||(X._default=[1,"div
","
"]),d.fn.extend({text:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.text(a.call(this,b,c.text()))});if(typeof a!=="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return d.text(this)},wrapAll:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapAll(a.call(this,b))});if(this[0]){var b=d(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapInner(a.call(this,b))});return this.each(function(){var b=d(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){d(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=d(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,d(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,e;(e=this[c])!=null;c++)if(!a||d.filter(a,[e]).length)!b&&e.nodeType===1&&(d.cleanData(e.getElementsByTagName("*")),d.cleanData([e])),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&d.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return d.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(P,""):null;if(typeof a!=="string"||V.test(a)||!d.support.leadingWhitespace&&Q.test(a)||X[(S.exec(a)||["",""])[1].toLowerCase()])d.isFunction(a)?this.each(function(b){var c=d(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);else{a=a.replace(R,"<$1>");try{for(var c=0,e=this.length;c1&&l0?this.clone(!0):this).get();d(f[h])[b](j),e=e.concat(j)}return this.pushStack(e,a,f.selector)}}),d.extend({clone:function(a,b,c){var e=a.cloneNode(!0),f,g,h;if((!d.support.noCloneEvent||!d.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!d.isXMLDoc(a)){$(a,e),f=_(a),g=_(e);for(h=0;f[h];++h)$(f[h],g[h])}if(b){Z(a,e);if(c){f=_(a),g=_(e);for(h=0;f[h];++h)Z(f[h],g[h])}}return e},clean:function(a,b,e,f){b=b||c,typeof b.createElement==="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var g=[];for(var h=0,i;(i=a[h])!=null;h++){typeof i==="number"&&(i+="");if(!i)continue;if(typeof i!=="string"||U.test(i)){if(typeof i==="string"){i=i.replace(R,"<$1>");var j=(S.exec(i)||["",""])[1].toLowerCase(),k=X[j]||X._default,l=k[0],m=b.createElement("div");m.innerHTML=k[1]+i+k[2];while(l--)m=m.lastChild;if(!d.support.tbody){var n=T.test(i),o=j==="table"&&!n?m.firstChild&&m.firstChild.childNodes:k[1]===""&&!n?m.childNodes:[];for(var p=o.length-1;p>=0;--p)d.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!d.support.leadingWhitespace&&Q.test(i)&&m.insertBefore(b.createTextNode(Q.exec(i)[0]),m.firstChild),i=m.childNodes}}else i=b.createTextNode(i);i.nodeType?g.push(i):g=d.merge(g,i)}if(e)for(h=0;g[h];h++)!f||!d.nodeName(g[h],"script")||g[h].type&&g[h].type.toLowerCase()!=="text/javascript"?(g[h].nodeType===1&&g.splice.apply(g,[h+1,0].concat(d.makeArray(g[h].getElementsByTagName("script")))),e.appendChild(g[h])):f.push(g[h].parentNode?g[h].parentNode.removeChild(g[h]):g[h]);return g},cleanData:function(a){var b,c,e=d.cache,f=d.expando,g=d.event.special,h=d.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&d.noData[j.nodeName.toLowerCase()])continue;c=j[d.expando];if(c){b=e[c]&&e[c][f];if(b&&b.events){for(var k in b.events)g[k]?d.event.remove(j,k):d.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[d.expando]:j.removeAttribute&&j.removeAttribute(d.expando),delete e[c]}}}});var bb=/alpha\([^)]*\)/i,bc=/opacity=([^)]*)/,bd=/-([a-z])/ig,be=/([A-Z])/g,bf=/^-?\d+(?:px)?$/i,bg=/^-?\d/,bh={position:"absolute",visibility:"hidden",display:"block"},bi=["Left","Right"],bj=["Top","Bottom"],bk,bl,bm,bn=function(a,b){return b.toUpperCase()};d.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return d.access(this,a,c,!0,function(a,c,e){return e!==b?d.style(a,c,e):d.css(a,c)})},d.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bk(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0},cssProps:{"float":d.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,e,f){if(a&&a.nodeType!==3&&a.nodeType!==8&&a.style){var g,h=d.camelCase(c),i=a.style,j=d.cssHooks[h];c=d.cssProps[h]||h;if(e===b){if(j&&"get"in j&&(g=j.get(a,!1,f))!==b)return g;return i[c]}if(typeof e==="number"&&isNaN(e)||e==null)return;typeof e==="number"&&!d.cssNumber[h]&&(e+="px");if(!j||!("set"in j)||(e=j.set(a,e))!==b)try{i[c]=e}catch(k){}}},css:function(a,c,e){var f,g=d.camelCase(c),h=d.cssHooks[g];c=d.cssProps[g]||g;if(h&&"get"in h&&(f=h.get(a,!0,e))!==b)return f;if(bk)return bk(a,c,g)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bd,bn)}}),d.curCSS=d.css,d.each(["height","width"],function(a,b){d.cssHooks[b]={get:function(a,c,e){var f;if(c){a.offsetWidth!==0?f=bo(a,b,e):d.swap(a,bh,function(){f=bo(a,b,e)});if(f<=0){f=bk(a,b,b),f==="0px"&&bm&&(f=bm(a,b,b));if(f!=null)return f===""||f==="auto"?"0px":f}if(f<0||f==null){f=a.style[b];return f===""||f==="auto"?"0px":f}return typeof f==="string"?f:f+"px"}},set:function(a,b){if(!bf.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),d.support.opacity||(d.cssHooks.opacity={get:function(a,b){return bc.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style;c.zoom=1;var e=d.isNaN(b)?"":"alpha(opacity="+b*100+")",f=c.filter||"";c.filter=bb.test(f)?f.replace(bb,e):c.filter+" "+e}}),c.defaultView&&c.defaultView.getComputedStyle&&(bl=function(a,c,e){var f,g,h;e=e.replace(be,"-$1").toLowerCase();if(!(g=a.ownerDocument.defaultView))return b;if(h=g.getComputedStyle(a,null))f=h.getPropertyValue(e),f===""&&!d.contains(a.ownerDocument.documentElement,a)&&(f=d.style(a,e));return f}),c.documentElement.currentStyle&&(bm=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bf.test(d)&&bg.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bk=bl||bm,d.expr&&d.expr.filters&&(d.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!d.support.reliableHiddenOffsets&&(a.style.display||d.css(a,"display"))==="none"},d.expr.filters.visible=function(a){return!d.expr.filters.hidden(a)});var bp=/%20/g,bq=/\[\]$/,br=/\r?\n/g,bs=/#.*$/,bt=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bu=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bv=/(?:^file|^widget|\-extension):$/,bw=/^(?:GET|HEAD)$/,bx=/^\/\//,by=/\?/,bz=/)<[^<]*)*<\/script>/gi,bA=/^(?:select|textarea)/i,bB=/\s+/,bC=/([?&])_=[^&]*/,bD=/(^|\-)([a-z])/g,bE=function(a,b,c){return b+c.toUpperCase()},bF=/^([\w\+\.\-]+:)\/\/([^\/?#:]*)(?::(\d+))?/,bG=d.fn.load,bH={},bI={},bJ,bK;try{bJ=c.location.href}catch(bL){bJ=c.createElement("a"),bJ.href="",bJ=bJ.href}bK=bF.exec(bJ.toLowerCase()),d.fn.extend({load:function(a,c,e){if(typeof a!=="string"&&bG)return bG.apply(this,arguments);if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var g=a.slice(f,a.length);a=a.slice(0,f)}var h="GET";c&&(d.isFunction(c)?(e=c,c=b):typeof c==="object"&&(c=d.param(c,d.ajaxSettings.traditional),h="POST"));var i=this;d.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?d("
").append(c.replace(bz,"")).find(g):c)),e&&i.each(e,[c,b,a])}});return this},serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?d.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bA.test(this.nodeName)||bu.test(this.type))}).map(function(a,b){var c=d(this).val();return c==null?null:d.isArray(c)?d.map(c,function(a,c){return{name:b.name,value:a.replace(br,"\r\n")}}):{name:b.name,value:c.replace(br,"\r\n")}}).get()}}),d.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){d.fn[b]=function(a){return this.bind(b,a)}}),d.each(["get","post"],function(a,c){d[c]=function(a,e,f,g){d.isFunction(e)&&(g=g||f,f=e,e=b);return d.ajax({type:c,url:a,data:e,success:f,dataType:g})}}),d.extend({getScript:function(a,c){return d.get(a,b,c,"script")},getJSON:function(a,b,c){return d.get(a,b,c,"json")},ajaxSetup:function(a,b){b?d.extend(!0,a,d.ajaxSettings,b):(b=a,a=d.extend(!0,d.ajaxSettings,b));for(var c in {context:1,url:1})c in b?a[c]=b[c]:c in d.ajaxSettings&&(a[c]=d.ajaxSettings[c]);return a},ajaxSettings:{url:bJ,isLocal:bv.test(bK[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML}},ajaxPrefilter:bM(bH),ajaxTransport:bM(bI),ajax:function(a,c){function v(a,c,l,n){if(r!==2){r=2,p&&clearTimeout(p),o=b,m=n||"",u.readyState=a?4:0;var q,t,v,w=l?bP(e,u,l):b,x,y;if(a>=200&&a<300||a===304){if(e.ifModified){if(x=u.getResponseHeader("Last-Modified"))d.lastModified[k]=x;if(y=u.getResponseHeader("Etag"))d.etag[k]=y}if(a===304)c="notmodified",q=!0;else try{t=bQ(e,w),c="success",q=!0}catch(z){c="parsererror",v=z}}else{v=c;if(!c||a)c="error",a<0&&(a=0)}u.status=a,u.statusText=c,q?h.resolveWith(f,[t,c,u]):h.rejectWith(f,[u,c,v]),u.statusCode(j),j=b,s&&g.trigger("ajax"+(q?"Success":"Error"),[u,e,q?t:v]),i.resolveWith(f,[u,c]),s&&(g.trigger("ajaxComplete",[u,e]),--d.active||d.event.trigger("ajaxStop"))}}typeof a==="object"&&(c=a,a=b),c=c||{};var e=d.ajaxSetup({},c),f=e.context||e,g=f!==e&&(f.nodeType||f instanceof d)?d(f):d.event,h=d.Deferred(),i=d._Deferred(),j=e.statusCode||{},k,l={},m,n,o,p,q,r=0,s,t,u={readyState:0,setRequestHeader:function(a,b){r||(l[a.toLowerCase().replace(bD,bE)]=b);return this},getAllResponseHeaders:function(){return r===2?m:null},getResponseHeader:function(a){var c;if(r===2){if(!n){n={};while(c=bt.exec(m))n[c[1].toLowerCase()]=c[2]}c=n[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){r||(e.mimeType=a);return this},abort:function(a){a=a||"abort",o&&o.abort(a),v(0,a);return this}};h.promise(u),u.success=u.done,u.error=u.fail,u.complete=i.done,u.statusCode=function(a){if(a){var b;if(r<2)for(b in a)j[b]=[j[b],a[b]];else b=a[u.status],u.then(b,b)}return this},e.url=((a||e.url)+"").replace(bs,"").replace(bx,bK[1]+"//"),e.dataTypes=d.trim(e.dataType||"*").toLowerCase().split(bB),e.crossDomain||(q=bF.exec(e.url.toLowerCase()),e.crossDomain=q&&(q[1]!=bK[1]||q[2]!=bK[2]||(q[3]||(q[1]==="http:"?80:443))!=(bK[3]||(bK[1]==="http:"?80:443)))),e.data&&e.processData&&typeof e.data!=="string"&&(e.data=d.param(e.data,e.traditional)),bN(bH,e,c,u);if(r===2)return!1;s=e.global,e.type=e.type.toUpperCase(),e.hasContent=!bw.test(e.type),s&&d.active++===0&&d.event.trigger("ajaxStart");if(!e.hasContent){e.data&&(e.url+=(by.test(e.url)?"&":"?")+e.data),k=e.url;if(e.cache===!1){var w=d.now(),x=e.url.replace(bC,"$1_="+w);e.url=x+(x===e.url?(by.test(e.url)?"&":"?")+"_="+w:"")}}if(e.data&&e.hasContent&&e.contentType!==!1||c.contentType)l["Content-Type"]=e.contentType;e.ifModified&&(k=k||e.url,d.lastModified[k]&&(l["If-Modified-Since"]=d.lastModified[k]),d.etag[k]&&(l["If-None-Match"]=d.etag[k])),l.Accept=e.dataTypes[0]&&e.accepts[e.dataTypes[0]]?e.accepts[e.dataTypes[0]]+(e.dataTypes[0]!=="*"?", */*; q=0.01":""):e.accepts["*"];for(t in e.headers)u.setRequestHeader(t,e.headers[t]);if(e.beforeSend&&(e.beforeSend.call(f,u,e)===!1||r===2)){u.abort();return!1}for(t in {success:1,error:1,complete:1})u[t](e[t]);o=bN(bI,e,c,u);if(o){u.readyState=1,s&&g.trigger("ajaxSend",[u,e]),e.async&&e.timeout>0&&(p=setTimeout(function(){u.abort("timeout")},e.timeout));try{r=1,o.send(l,v)}catch(y){status<2?v(-1,y):d.error(y)}}else v(-1,"No Transport");return u},param:function(a,c){var e=[],f=function(a,b){b=d.isFunction(b)?b():b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=d.ajaxSettings.traditional);if(d.isArray(a)||a.jquery&&!d.isPlainObject(a))d.each(a,function(){f(this.name,this.value)});else for(var g in a)bO(g,a[g],c,f);return e.join("&").replace(bp,"+")}}),d.extend({active:0,lastModified:{},etag:{}});var bR=d.now(),bS=/(\=)\?(&|$)|()\?\?()/i;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return d.expando+"_"+bR++}}),d.ajaxPrefilter("json jsonp",function(b,c,e){var f=typeof b.data==="string";if(b.dataTypes[0]==="jsonp"||c.jsonpCallback||c.jsonp!=null||b.jsonp!==!1&&(bS.test(b.url)||f&&bS.test(b.data))){var g,h=b.jsonpCallback=d.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2",m=function(){a[h]=i,g&&d.isFunction(i)&&a[h](g[0])};b.jsonp!==!1&&(j=j.replace(bS,l),b.url===j&&(f&&(k=k.replace(bS,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},e.then(m,m),b.converters["script json"]=function(){g||d.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){d.globalEval(a);return a}}}),d.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),d.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var bT=d.now(),bU,bV;d.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&bX()||bY()}:bX,bV=d.ajaxSettings.xhr(),d.support.ajax=!!bV,d.support.cors=bV&&"withCredentials"in bV,bV=b,d.support.ajax&&d.ajaxTransport(function(a){if(!a.crossDomain||d.support.cors){var c;return{send:function(e,f){var g=a.xhr(),h,i;a.username?g.open(a.type,a.url,a.async,a.username,a.password):g.open(a.type,a.url,a.async);if(a.xhrFields)for(i in a.xhrFields)g[i]=a.xhrFields[i];a.mimeType&&g.overrideMimeType&&g.overrideMimeType(a.mimeType),(!a.crossDomain||a.hasContent)&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(i in e)g.setRequestHeader(i,e[i])}catch(j){}g.send(a.hasContent&&a.data||null),c=function(e,i){var j,k,l,m,n;try{if(c&&(i||g.readyState===4)){c=b,h&&(g.onreadystatechange=d.noop,delete bU[h]);if(i)g.readyState!==4&&g.abort();else{j=g.status,l=g.getAllResponseHeaders(),m={},n=g.responseXML,n&&n.documentElement&&(m.xml=n),m.text=g.responseText;try{k=g.statusText}catch(o){k=""}j||!a.isLocal||a.crossDomain?j===1223&&(j=204):j=m.text?200:404}}}catch(p){i||f(-1,p)}m&&f(j,k,m,l)},a.async&&g.readyState!==4?(bU||(bU={},bW()),h=bT++,g.onreadystatechange=bU[h]=c):c()},abort:function(){c&&c(0,1)}}}});var bZ={},b$=/^(?:toggle|show|hide)$/,b_=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,ca,cb=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];d.fn.extend({show:function(a,b,c){var e,f;if(a||a===0)return this.animate(cc("show",3),a,b,c);for(var g=0,h=this.length;g=0;a--)c[a].elem===this&&(b&&c[a](!0),c.splice(a,1))}),b||this.dequeue();return this}}),d.each({slideDown:cc("show",1),slideUp:cc("hide",1),slideToggle:cc("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){d.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),d.extend({speed:function(a,b,c){var e=a&&typeof a==="object"?d.extend({},a):{complete:c||!c&&b||d.isFunction(a)&&a,duration:a,easing:c&&b||b&&!d.isFunction(b)&&b};e.duration=d.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in d.fx.speeds?d.fx.speeds[e.duration]:d.fx.speeds._default,e.old=e.complete,e.complete=function(){e.queue!==!1&&d(this).dequeue(),d.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig||(b.orig={})}}),d.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(d.fx.step[this.prop]||d.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=d.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function g(a){return e.step(a)}var e=this,f=d.fx;this.startTime=d.now(),this.start=a,this.end=b,this.unit=c||this.unit||(d.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,g.elem=this.elem,g()&&d.timers.push(g)&&!ca&&(ca=setInterval(f.tick,f.interval))},show:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),d(this.elem).show()},hide:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=d.now(),c=!0;if(a||b>=this.options.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),this.options.curAnim[this.prop]=!0;for(var e in this.options.curAnim)this.options.curAnim[e]!==!0&&(c=!1);if(c){if(this.options.overflow!=null&&!d.support.shrinkWrapBlocks){var f=this.elem,g=this.options;d.each(["","X","Y"],function(a,b){f.style["overflow"+b]=g.overflow[a]})}this.options.hide&&d(this.elem).hide();if(this.options.hide||this.options.show)for(var h in this.options.curAnim)d.style(this.elem,h,this.options.orig[h]);this.options.complete.call(this.elem)}return!1}var i=b-this.startTime;this.state=i/this.options.duration;var j=this.options.specialEasing&&this.options.specialEasing[this.prop],k=this.options.easing||(d.easing.swing?"swing":"linear");this.pos=d.easing[j||k](this.state,i,0,1,this.options.duration),this.now=this.start+(this.end-this.start)*this.pos,this.update();return!0}},d.extend(d.fx,{tick:function(){var a=d.timers;for(var b=0;b
";d.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),e=b.firstChild,f=e.firstChild,h=e.nextSibling.firstChild.firstChild,this.doesNotAddBorder=f.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,f.style.position="fixed",f.style.top="20px",this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15,f.style.position=f.style.top="",e.style.overflow="hidden",e.style.position="relative",this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),a=b=e=f=g=h=null,d.offset.initialize=d.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;d.offset.initialize(),d.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(d.css(a,"marginTop"))||0,c+=parseFloat(d.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var e=d.css(a,"position");e==="static"&&(a.style.position="relative");var f=d(a),g=f.offset(),h=d.css(a,"top"),i=d.css(a,"left"),j=e==="absolute"&&d.inArray("auto",[h,i])>-1,k={},l={},m,n;j&&(l=f.position()),m=j?l.top:parseInt(h,10)||0,n=j?l.left:parseInt(i,10)||0,d.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):f.css(k)}},d.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),e=cf.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(d.css(a,"marginTop"))||0,c.left-=parseFloat(d.css(a,"marginLeft"))||0,e.top+=parseFloat(d.css(b[0],"borderTopWidth"))||0,e.left+=parseFloat(d.css(b[0],"borderLeftWidth"))||0;return{top:c.top-e.top,left:c.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&(!cf.test(a.nodeName)&&d.css(a,"position")==="static"))a=a.offsetParent;return a})}}),d.each(["Left","Top"],function(a,c){var e="scroll"+c;d.fn[e]=function(c){var f=this[0],g;if(!f)return null;if(c!==b)return this.each(function(){g=cg(this),g?g.scrollTo(a?d(g).scrollLeft():c,a?c:d(g).scrollTop()):this[e]=c});g=cg(f);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:d.support.boxModel&&g.document.documentElement[e]||g.document.body[e]:f[e]}}),d.each(["Height","Width"],function(a,c){var e=c.toLowerCase();d.fn["inner"+c]=function(){return this[0]?parseFloat(d.css(this[0],e,"padding")):null},d.fn["outer"+c]=function(a){return this[0]?parseFloat(d.css(this[0],e,a?"margin":"border")):null},d.fn[e]=function(a){var f=this[0];if(!f)return a==null?null:this;if(d.isFunction(a))return this.each(function(b){var c=d(this);c[e](a.call(this,b,c[e]()))});if(d.isWindow(f)){var g=f.document.documentElement["client"+c];return f.document.compatMode==="CSS1Compat"&&g||f.document.body["client"+c]||g}if(f.nodeType===9)return Math.max(f.documentElement["client"+c],f.body["scroll"+c],f.documentElement["scroll"+c],f.body["offset"+c],f.documentElement["offset"+c]);if(a===b){var h=d.css(f,e),i=parseFloat(h);return d.isNaN(i)?h:i}return this.css(e,typeof a==="string"?a:a+"px")}}),a.jQuery=a.$=d})(window); \ No newline at end of file From e260daf447159c51df9bbbab203937d2ecb90aa7 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Thu, 10 Mar 2011 16:05:16 +0300 Subject: [PATCH 051/115] Applying config.confirm_resolve_err option to errs list. --- app/helpers/errs_helper.rb | 5 ++++- app/views/errs/_table.html.haml | 2 +- app/views/errs/show.html.haml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/helpers/errs_helper.rb b/app/helpers/errs_helper.rb index ff56309e88..a84e12d182 100644 --- a/app/helpers/errs_helper.rb +++ b/app/helpers/errs_helper.rb @@ -3,5 +3,8 @@ module ErrsHelper def last_notice_at err err.last_notice_at || err.created_at end - + + def err_confirm + Errbit::Config.confirm_resolve_err === false ? nil : 'Seriously?' + end end \ No newline at end of file diff --git a/app/views/errs/_table.html.haml b/app/views/errs/_table.html.haml index 3bd09f352d..28efc14f37 100644 --- a/app/views/errs/_table.html.haml +++ b/app/views/errs/_table.html.haml @@ -19,7 +19,7 @@ %td.latest #{time_ago_in_words(last_notice_at err)} ago %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a' %td.count= link_to err.notices.count, app_err_path(err.app, err) - %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(err.app, err), :title => "Resolve", :method => :put, :confirm => 'Seriously?', :class => 'resolve' if err.unresolved? + %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? - if errs.none? %tr %td{:colspan => (@app ? 5 : 6)} diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index bcf1e6eec3..62f533c7c7 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -8,7 +8,7 @@ %strong Last Notice: = last_notice_at(@err).to_s(:micro) - content_for :action_bar do - %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => (Errbit::Config.confirm_resolve_err === false ? nil : 'Seriously?'), :class => 'resolve' if @err.unresolved? + %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' if @err.unresolved? %h4= @notice.try(:message) From 391a1271e869a0d4a95316e5478ac40d54f0dc1a Mon Sep 17 00:00:00 2001 From: Philip Hallstrom Date: Thu, 10 Mar 2011 15:20:07 -0800 Subject: [PATCH 052/115] Modify err_notification email subject to include the Rails environment. Add test for this and add one to ensure app name is there as well --- app/mailers/mailer.rb | 4 ++-- spec/controllers/notices_controller_spec.rb | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 06f65eee7d..1cf92dfce3 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -7,7 +7,7 @@ def err_notification(notice) mail({ :to => @app.watchers.map(&:address), - :subject => "[#{@app.name}] #{@notice.err.message}" + :subject => "[#{@app.name}][#{@notice.err.environment}] #{@notice.err.message}" }) end @@ -21,4 +21,4 @@ def deploy_notification(deploy) }) end -end \ No newline at end of file +end diff --git a/spec/controllers/notices_controller_spec.rb b/spec/controllers/notices_controller_spec.rb index 8e632dcec4..1b9980ef0e 100644 --- a/spec/controllers/notices_controller_spec.rb +++ b/spec/controllers/notices_controller_spec.rb @@ -24,6 +24,8 @@ email = ActionMailer::Base.deliveries.last email.to.should include(@app.watchers.first.email) email.subject.should include(@notice.err.message) + email.subject.should include("[#{@app.name}]") + email.subject.should include("[#{@notice.err.environment}]") end end From e18f8d099bf9d0370294c7e20e7b539445cbb0e1 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Fri, 11 Mar 2011 23:37:29 +0300 Subject: [PATCH 053/115] Adding Gemfile.lock back since Heroku wants it. --- .gitignore | 1 - Gemfile.lock | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 Gemfile.lock diff --git a/.gitignore b/.gitignore index 33a647d6ad..6137e75503 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ config/deploy.rb config/mongoid.yml .rvmrc *~ -Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..ea9772b56d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,118 @@ +GEM + remote: http://rubygems.org/ + specs: + abstract (1.0.0) + actionmailer (3.0.3) + actionpack (= 3.0.3) + mail (~> 2.2.9) + actionpack (3.0.3) + activemodel (= 3.0.3) + activesupport (= 3.0.3) + builder (~> 2.1.2) + erubis (~> 2.6.6) + i18n (~> 0.4) + rack (~> 1.2.1) + rack-mount (~> 0.6.13) + rack-test (~> 0.5.6) + tzinfo (~> 0.3.23) + activemodel (3.0.3) + activesupport (= 3.0.3) + builder (~> 2.1.2) + i18n (~> 0.4) + activerecord (3.0.3) + activemodel (= 3.0.3) + activesupport (= 3.0.3) + arel (~> 2.0.2) + tzinfo (~> 0.3.23) + activeresource (3.0.3) + activemodel (= 3.0.3) + activesupport (= 3.0.3) + activesupport (3.0.3) + arel (2.0.9) + bcrypt-ruby (2.1.4) + bson (1.2.4) + bson_ext (1.2.4) + builder (2.1.2) + database_cleaner (0.6.5) + devise (1.1.7) + bcrypt-ruby (~> 2.1.2) + warden (~> 1.0.2) + diff-lcs (1.1.2) + erubis (2.6.6) + abstract (>= 1.0.0) + factory_girl (1.3.3) + factory_girl_rails (1.0.1) + factory_girl (~> 1.3) + railties (>= 3.0.0) + haml (3.0.25) + i18n (0.5.0) + mail (2.2.15) + activesupport (>= 2.3.6) + i18n (>= 0.4.0) + mime-types (~> 1.16) + treetop (~> 1.4.8) + mime-types (1.16) + mongo (1.2.4) + bson (>= 1.2.4) + mongoid (2.0.0.rc.7) + activemodel (~> 3.0) + mongo (~> 1.2) + tzinfo (~> 0.3.22) + will_paginate (~> 3.0.pre) + nokogiri (1.4.4) + polyglot (0.3.1) + rack (1.2.1) + rack-mount (0.6.13) + rack (>= 1.0.0) + rack-test (0.5.7) + rack (>= 1.0) + rails (3.0.3) + actionmailer (= 3.0.3) + actionpack (= 3.0.3) + activerecord (= 3.0.3) + activeresource (= 3.0.3) + activesupport (= 3.0.3) + bundler (~> 1.0) + railties (= 3.0.3) + railties (3.0.3) + actionpack (= 3.0.3) + activesupport (= 3.0.3) + rake (>= 0.8.7) + thor (~> 0.14.4) + rake (0.8.7) + rspec (2.5.0) + rspec-core (~> 2.5.0) + rspec-expectations (~> 2.5.0) + rspec-mocks (~> 2.5.0) + rspec-core (2.5.1) + rspec-expectations (2.5.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.5.0) + rspec-rails (2.5.0) + actionpack (~> 3.0) + activesupport (~> 3.0) + railties (~> 3.0) + rspec (~> 2.5.0) + thor (0.14.6) + treetop (1.4.9) + polyglot (>= 0.3.1) + tzinfo (0.3.24) + warden (1.0.3) + rack (>= 1.0.0) + will_paginate (3.0.pre2) + +PLATFORMS + ruby + +DEPENDENCIES + bson_ext (~> 1.2) + database_cleaner (~> 0.6.0) + devise (~> 1.1.3) + factory_girl_rails + haml + mongoid (~> 2.0.0.rc.6) + nokogiri + rails (= 3.0.3) + rspec (~> 2.1) + rspec-rails (~> 2.1) + will_paginate From 652b42f94786123b9942034e0484cb148ab78754 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Sat, 12 Mar 2011 01:00:52 +0300 Subject: [PATCH 054/115] Fixing layout width. --- public/stylesheets/application.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 62528c3e89..e6e480944b 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -35,7 +35,7 @@ a:hover { color: #0069cc; text-decoration: underline; } a.action { float: right; font-size: 0.9em;} #header > div, #nav-bar, #content-wrapper, #footer { - width: 900px; + width: 930px; margin: 0 auto; position: relative; } From d0bad87e92ae1c1d259f9a1cc05f75cb7f63e907 Mon Sep 17 00:00:00 2001 From: Sergey Avseyev Date: Mon, 14 Mar 2011 19:23:19 +0800 Subject: [PATCH 055/115] update README.md: trailing spaces & fix preformatted snippets --- README.md | 67 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index c258351bf9..d86203d479 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ Errbit: The open source self-hosted error catcher ================================================= -Errbit is an open source, self-hosted error catcher. It is [Hoptoad](http://hoptoadapp.com) -API compliant so you can just point the Hoptoad notifier at your Errbit server if you are +Errbit is an open source, self-hosted error catcher. It is [Hoptoad](http://hoptoadapp.com) +API compliant so you can just point the Hoptoad notifier at your Errbit server if you are already using Hoptoad. Errbit may be a good fit for you if: @@ -22,71 +22,70 @@ Installation *Note*: This app is intended for people with experience deploying and maintining Rails applications. If you're uncomfortable with any step below then Errbit is not -for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at +for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at [Thoughtbot](http://thoughtbot.com), which Errbit is based on. **Set your local box or server(Ubuntu):** - 1. Install MongoDB - * Follow the directions [here](http://www.mongodb.org/display/DOCS/Ubuntu+and+Debian+packages), then: - - aptitude update - aptitude install mongodb - + 1. Install MongoDB. Follow the directions [here](http://www.mongodb.org/display/DOCS/Ubuntu+and+Debian+packages), then: + + aptitude update + aptitude install mongodb + 2. Install libxml - - apt-get install libxml2 libxml2-dev libxslt-dev - + + apt-get install libxml2 libxml2-dev libxslt-dev + 3. Install Bundler - + gem install bundler - + **Running Locally:** 1. Bootstrap Errbit. This will copy over config.yml and also seed the database. - rake errbit:bootstrap + rake errbit:bootstrap 2. Update the config.yml and mongoid.yml files with information about your environment 3. Install dependencies - - bundle install - + + bundle install + 4. Start Server - - script/rails server + + script/rails server **Deploying:** 1. Bootstrap Errbit. This will copy over config.yml and also seed the database. - rake errbit:bootstrap + rake errbit:bootstrap 2. Update the deploy.rb file with information about your server 3. Setup server and deploy - - cap deploy:setup deploy + + cap deploy:setup deploy **Deploying to Heroku:** 1. Clone the repository - git clone http://github.com/jdpace/errbit.git + git clone http://github.com/jdpace/errbit.git 2. Create & configure for Heroku - gem install heroku - heroku create - heroku addons:add mongohq:free - heroku addons:add sendgrid:free - heroku config:add HEROKU=true - heroku config:add ERRBIT_HOST=some-hostname.example.com - heroku config:add ERRBIT_EMAIL_FROM=example@example.com - git push heroku master + gem install heroku + heroku create + heroku addons:add mongohq:free + heroku addons:add sendgrid:free + heroku config:add HEROKU=true + heroku config:add ERRBIT_HOST=some-hostname.example.com + heroku config:add ERRBIT_EMAIL_FROM=example@example.com + git push heroku master 3. Seed the DB (_NOTE_: No bootstrap task is used on Heroku!) - heroku rake db:seed + heroku rake db:seed 4. Enjoy! @@ -104,7 +103,7 @@ Special Thanks Contributing ------------ - + * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a From 9538c3877ff8675a1253ba649cd2e76f6e4e316c Mon Sep 17 00:00:00 2001 From: Jared Pace Date: Wed, 16 Mar 2011 06:04:29 -0700 Subject: [PATCH 056/115] Call out oruen as a core contributor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d86203d479..6557b7c969 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Special Thanks -------------- * [Michael Parenteau](http://michaelparenteau.com) - For rocking the Errbit design and providing a great user experience. +* [Nick Recobra aka oruen](https://github.com/oruen) - Nick is Errbit's first core contributor. He's been working hard at making Errbit more awesome. * [Relevance](http://thinkrelevance.com) - For giving me Open-source Fridays to work on Errbit and all my awesome co-workers for giving feedback and inspiration. * [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Hoptoad](http://hoptoadapp.com). From ee7d973ca71784e3b46374398f93ea37d72bacb6 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 16 Mar 2011 17:56:38 +0300 Subject: [PATCH 057/115] Fixed app renaming by not using app name as a key. --- app/models/app.rb | 3 ++- app/views/errs/show.html.haml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/app.rb b/app/models/app.rb index 42c317ad18..d928bc0770 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -5,7 +5,8 @@ class App field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false - key :name + # Some legacy apps may have sting as key instead of BSON::ObjectID + identity :type => String embeds_many :watchers embeds_many :deploys diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index 62f533c7c7..11b4fb928d 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -1,6 +1,8 @@ - content_for :page_title, @err.message - content_for :title, @err.klass - content_for :meta do + %strong App: + = @app.name %strong Where: = @err.where %strong Environment: From 77e148ca5e3b1793bea63ddbc1ae45ac4ef1e95d Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 16 Mar 2011 18:09:20 +0300 Subject: [PATCH 058/115] Turning off long running spec. --- spec/controllers/errs_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index ad2bbf2694..3952604334 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -33,6 +33,7 @@ end it "should handle lots of errors" do + pending "Turning off long running spec" 1000.times { Factory :notice } lambda { get :index }.should_not raise_error end From 01121261cb277ef9bd3f4c2b50a99ea137c10780 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 16 Mar 2011 18:21:54 +0300 Subject: [PATCH 059/115] Rails.js bump. --- public/javascripts/rails.js | 290 ++++++++++++++++++++---------------- 1 file changed, 158 insertions(+), 132 deletions(-) diff --git a/public/javascripts/rails.js b/public/javascripts/rails.js index 103032a557..dd0a47ec70 100644 --- a/public/javascripts/rails.js +++ b/public/javascripts/rails.js @@ -1,132 +1,158 @@ -jQuery(function ($) { - var csrf_token = $('meta[name=csrf-token]').attr('content'), - csrf_param = $('meta[name=csrf-param]').attr('content'); - - $.fn.extend({ - /** - * Triggers a custom event on an element and returns the event result - * this is used to get around not being able to ensure callbacks are placed - * at the end of the chain. - * - * TODO: deprecate with jQuery 1.4.2 release, in favor of subscribing to our - * own events and placing ourselves at the end of the chain. - */ - triggerAndReturn: function (name, data) { - var event = new $.Event(name); - this.trigger(event, data); - - return event.result !== false; - }, - - /** - * Handles execution of remote calls firing overridable events along the way - */ - callRemote: function () { - var el = this, - method = el.attr('method') || el.attr('data-method') || 'GET', - url = el.attr('action') || el.attr('href'), - dataType = el.attr('data-type') || 'script'; - - if (url === undefined) { - throw "No URL specified for remote call (action or href must be present)."; - } else { - if (el.triggerAndReturn('ajax:before')) { - var data = el.is('form') ? el.serializeArray() : []; - $.ajax({ - url: url, - data: data, - dataType: dataType, - type: method.toUpperCase(), - beforeSend: function (xhr) { - el.trigger('ajax:loading', xhr); - }, - success: function (data, status, xhr) { - el.trigger('ajax:success', [data, status, xhr]); - }, - complete: function (xhr) { - el.trigger('ajax:complete', xhr); - }, - error: function (xhr, status, error) { - el.trigger('ajax:failure', [xhr, status, error]); - } - }); - } - - el.trigger('ajax:after'); - } - } - }); - - /** - * confirmation handler - */ - $('a[data-confirm],input[data-confirm]').live('click', function () { - var el = $(this); - if (el.triggerAndReturn('confirm')) { - if (!confirm(el.attr('data-confirm'))) { - return false; - } - } - }); - - - /** - * remote handlers - */ - $('form[data-remote]').live('submit', function (e) { - $(this).callRemote(); - e.preventDefault(); - }); - - $('a[data-remote],input[data-remote]').live('click', function (e) { - $(this).callRemote(); - e.preventDefault(); - }); - - $('a[data-method]:not([data-remote])').live('click', function (e){ - var link = $(this), - href = link.attr('href'), - method = link.attr('data-method'), - form = $('
'), - metadata_input = ''; - - if (csrf_param != null && csrf_token != null) { - metadata_input += ''; - } - - form.hide() - .append(metadata_input) - .appendTo('body'); - - e.preventDefault(); - form.submit(); - }); - - /** - * disable-with handlers - */ - var disable_with_input_selector = 'input[data-disable-with]'; - var disable_with_form_remote_selector = 'form[data-remote]:has(' + disable_with_input_selector + ')'; - var disable_with_form_not_remote_selector = 'form:not([data-remote]):has(' + disable_with_input_selector + ')'; - - var disable_with_input_function = function () { - $(this).find(disable_with_input_selector).each(function () { - var input = $(this); - input.data('enable-with', input.val()) - .attr('value', input.attr('data-disable-with')) - .attr('disabled', 'disabled'); - }); - }; - - $(disable_with_form_remote_selector).live('ajax:before', disable_with_input_function); - $(disable_with_form_not_remote_selector).live('submit', disable_with_input_function); - - $(disable_with_form_remote_selector).live('ajax:complete', function () { - $(this).find(disable_with_input_selector).each(function () { - var input = $(this); - input.removeAttr('disabled') - .val(input.data('enable-with')); - }); - }); - -}); \ No newline at end of file +/** + * Unobtrusive scripting adapter for jQuery + * + * Requires jQuery 1.4.3 or later. + * https://github.com/rails/jquery-ujs + */ + +(function($) { + // Make sure that every Ajax request sends the CSRF token + function CSRFProtection(xhr) { + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) xhr.setRequestHeader('X-CSRF-Token', token); + } + if ('ajaxPrefilter' in $) $.ajaxPrefilter(function(options, originalOptions, xhr){ CSRFProtection(xhr) }); + else $(document).ajaxSend(function(e, xhr){ CSRFProtection(xhr) }); + + // Triggers an event on an element and returns the event result + function fire(obj, name, data) { + var event = $.Event(name); + obj.trigger(event, data); + return event.result !== false; + } + + // Submits "remote" forms and links with ajax + function handleRemote(element) { + var method, url, data, + dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType); + + if (fire(element, 'ajax:before')) { + if (element.is('form')) { + method = element.attr('method'); + url = element.attr('action'); + data = element.serializeArray(); + // memoized value from clicked submit button + var button = element.data('ujs:submit-button'); + if (button) { + data.push(button); + element.data('ujs:submit-button', null); + } + } else { + method = element.data('method'); + url = element.attr('href'); + data = null; + } + $.ajax({ + url: url, type: method || 'GET', data: data, dataType: dataType, + // stopping the "ajax:beforeSend" event will cancel the ajax request + beforeSend: function(xhr, settings) { + if (settings.dataType === undefined) { + xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); + } + return fire(element, 'ajax:beforeSend', [xhr, settings]); + }, + success: function(data, status, xhr) { + element.trigger('ajax:success', [data, status, xhr]); + }, + complete: function(xhr, status) { + element.trigger('ajax:complete', [xhr, status]); + }, + error: function(xhr, status, error) { + element.trigger('ajax:error', [xhr, status, error]); + } + }); + } + } + + // Handles "data-method" on links such as: + // Delete + function handleMethod(link) { + var href = link.attr('href'), + method = link.data('method'), + csrf_token = $('meta[name=csrf-token]').attr('content'), + csrf_param = $('meta[name=csrf-param]').attr('content'), + form = $('
'), + metadata_input = ''; + + if (csrf_param !== undefined && csrf_token !== undefined) { + metadata_input += ''; + } + + form.hide().append(metadata_input).appendTo('body'); + form.submit(); + } + + function disableFormElements(form) { + form.find('input[data-disable-with]').each(function() { + var input = $(this); + input.data('ujs:enable-with', input.val()) + .val(input.data('disable-with')) + .attr('disabled', 'disabled'); + }); + } + + function enableFormElements(form) { + form.find('input[data-disable-with]').each(function() { + var input = $(this); + input.val(input.data('ujs:enable-with')).removeAttr('disabled'); + }); + } + + function allowAction(element) { + var message = element.data('confirm'); + return !message || (fire(element, 'confirm') && confirm(message)); + } + + function requiredValuesMissing(form) { + var missing = false; + form.find('input[name][required]').each(function() { + if (!$(this).val()) missing = true; + }); + return missing; + } + + $('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) { + var link = $(this); + if (!allowAction(link)) return false; + + if (link.data('remote') != undefined) { + handleRemote(link); + return false; + } else if (link.data('method')) { + handleMethod(link); + return false; + } + }); + + $('form').live('submit.rails', function(e) { + var form = $(this), remote = form.data('remote') != undefined; + if (!allowAction(form)) return false; + + // skip other logic when required values are missing + if (requiredValuesMissing(form)) return !remote; + + if (remote) { + handleRemote(form); + return false; + } else { + // slight timeout so that the submit button gets properly serialized + setTimeout(function(){ disableFormElements(form) }, 13); + } + }); + + $('form input[type=submit], form button[type=submit], form button:not([type])').live('click.rails', function() { + var button = $(this); + if (!allowAction(button)) return false; + // register the pressed submit button + var name = button.attr('name'), data = name ? {name:name, value:button.val()} : null; + button.closest('form').data('ujs:submit-button', data); + }); + + $('form').live('ajax:beforeSend.rails', function(event) { + if (this == event.target) disableFormElements($(this)); + }); + + $('form').live('ajax:complete.rails', function(event) { + if (this == event.target) enableFormElements($(this)); + }); +})( jQuery ); \ No newline at end of file From 78becdf15894f7e5ca4657a1149ec745dcf956e4 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 16 Mar 2011 19:58:22 +0300 Subject: [PATCH 060/115] Generating app id on create so Mongoid won't consider it a BSON::ObjectID. --- app/models/app.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/app.rb b/app/models/app.rb index d928bc0770..e7b3fae8bf 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -7,7 +7,13 @@ class App field :resolve_errs_on_deploy, :type => Boolean, :default => false # Some legacy apps may have sting as key instead of BSON::ObjectID identity :type => String - + # There seems to be a Mongoid bug making it impossible to use String identity with references_many feature: + # https://github.com/mongoid/mongoid/issues/703 + # Using 32 character string as a workaround. + before_save :on => :create do |r| + r.id = ActiveSupport::SecureRandom.hex + end + embeds_many :watchers embeds_many :deploys references_many :errs, :dependent => :destroy From a3976b02ae3ce8497818908253763a1a3ffb645d Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Sun, 20 Mar 2011 10:17:14 +0100 Subject: [PATCH 061/115] Update Rails to 3.0.5 --- Gemfile | 2 +- Gemfile.lock | 58 ++++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Gemfile b/Gemfile index 0d8d436cb9..8d2a55cfcc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'http://rubygems.org' -gem 'rails', '3.0.3' +gem 'rails', '3.0.5' gem 'nokogiri' gem 'mongoid', '~> 2.0.0.rc.6' gem 'haml' diff --git a/Gemfile.lock b/Gemfile.lock index ea9772b56d..abfaf75cb0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,32 +2,32 @@ GEM remote: http://rubygems.org/ specs: abstract (1.0.0) - actionmailer (3.0.3) - actionpack (= 3.0.3) - mail (~> 2.2.9) - actionpack (3.0.3) - activemodel (= 3.0.3) - activesupport (= 3.0.3) + actionmailer (3.0.5) + actionpack (= 3.0.5) + mail (~> 2.2.15) + actionpack (3.0.5) + activemodel (= 3.0.5) + activesupport (= 3.0.5) builder (~> 2.1.2) erubis (~> 2.6.6) i18n (~> 0.4) rack (~> 1.2.1) rack-mount (~> 0.6.13) - rack-test (~> 0.5.6) + rack-test (~> 0.5.7) tzinfo (~> 0.3.23) - activemodel (3.0.3) - activesupport (= 3.0.3) + activemodel (3.0.5) + activesupport (= 3.0.5) builder (~> 2.1.2) i18n (~> 0.4) - activerecord (3.0.3) - activemodel (= 3.0.3) - activesupport (= 3.0.3) + activerecord (3.0.5) + activemodel (= 3.0.5) + activesupport (= 3.0.5) arel (~> 2.0.2) tzinfo (~> 0.3.23) - activeresource (3.0.3) - activemodel (= 3.0.3) - activesupport (= 3.0.3) - activesupport (3.0.3) + activeresource (3.0.5) + activemodel (= 3.0.5) + activesupport (= 3.0.5) + activesupport (3.0.5) arel (2.0.9) bcrypt-ruby (2.1.4) bson (1.2.4) @@ -61,22 +61,22 @@ GEM will_paginate (~> 3.0.pre) nokogiri (1.4.4) polyglot (0.3.1) - rack (1.2.1) + rack (1.2.2) rack-mount (0.6.13) rack (>= 1.0.0) rack-test (0.5.7) rack (>= 1.0) - rails (3.0.3) - actionmailer (= 3.0.3) - actionpack (= 3.0.3) - activerecord (= 3.0.3) - activeresource (= 3.0.3) - activesupport (= 3.0.3) + rails (3.0.5) + actionmailer (= 3.0.5) + actionpack (= 3.0.5) + activerecord (= 3.0.5) + activeresource (= 3.0.5) + activesupport (= 3.0.5) bundler (~> 1.0) - railties (= 3.0.3) - railties (3.0.3) - actionpack (= 3.0.3) - activesupport (= 3.0.3) + railties (= 3.0.5) + railties (3.0.5) + actionpack (= 3.0.5) + activesupport (= 3.0.5) rake (>= 0.8.7) thor (~> 0.14.4) rake (0.8.7) @@ -96,7 +96,7 @@ GEM thor (0.14.6) treetop (1.4.9) polyglot (>= 0.3.1) - tzinfo (0.3.24) + tzinfo (0.3.25) warden (1.0.3) rack (>= 1.0.0) will_paginate (3.0.pre2) @@ -112,7 +112,7 @@ DEPENDENCIES haml mongoid (~> 2.0.0.rc.6) nokogiri - rails (= 3.0.3) + rails (= 3.0.5) rspec (~> 2.1) rspec-rails (~> 2.1) will_paginate From b643a04590a6a98b53125f8fd714d22919465211 Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Sun, 20 Mar 2011 10:19:12 +0100 Subject: [PATCH 062/115] Update Devise to 1.1.8 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 8d2a55cfcc..e642819e28 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ gem 'nokogiri' gem 'mongoid', '~> 2.0.0.rc.6' gem 'haml' gem 'will_paginate' -gem 'devise', '~> 1.1.3' +gem 'devise', '~> 1.1.8' platform :ruby do gem 'bson_ext', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index abfaf75cb0..010f82b2f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,7 +34,7 @@ GEM bson_ext (1.2.4) builder (2.1.2) database_cleaner (0.6.5) - devise (1.1.7) + devise (1.1.8) bcrypt-ruby (~> 2.1.2) warden (~> 1.0.2) diff-lcs (1.1.2) @@ -107,7 +107,7 @@ PLATFORMS DEPENDENCIES bson_ext (~> 1.2) database_cleaner (~> 0.6.0) - devise (~> 1.1.3) + devise (~> 1.1.8) factory_girl_rails haml mongoid (~> 2.0.0.rc.6) From b2c779faf92b2081ce3587a58a6c5516d43bac73 Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Sun, 20 Mar 2011 10:20:33 +0100 Subject: [PATCH 063/115] Update Mongoid to 2.0.0.rc.7 --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index e642819e28..c5cc6e705c 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'nokogiri' -gem 'mongoid', '~> 2.0.0.rc.6' +gem 'mongoid', '~> 2.0.0.rc.7' gem 'haml' gem 'will_paginate' gem 'devise', '~> 1.1.8' diff --git a/Gemfile.lock b/Gemfile.lock index 010f82b2f3..85be901aaa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -110,7 +110,7 @@ DEPENDENCIES devise (~> 1.1.8) factory_girl_rails haml - mongoid (~> 2.0.0.rc.6) + mongoid (~> 2.0.0.rc.7) nokogiri rails (= 3.0.5) rspec (~> 2.1) From d0b9c018ee931ba0aef4738758f496ece05957de Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Sun, 20 Mar 2011 10:27:38 +0100 Subject: [PATCH 064/115] Update RSpec and RSpec-rails to 2.5 --- Gemfile | 4 ++-- Gemfile.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index c5cc6e705c..7f027659e1 100644 --- a/Gemfile +++ b/Gemfile @@ -12,11 +12,11 @@ platform :ruby do end group :development, :test do - gem 'rspec-rails', '~> 2.1' + gem 'rspec-rails', '~> 2.5' end group :test do - gem 'rspec', '~> 2.1' + gem 'rspec', '~> 2.5' gem 'database_cleaner', '~> 0.6.0' gem 'factory_girl_rails' end diff --git a/Gemfile.lock b/Gemfile.lock index 85be901aaa..85800ec0de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,6 @@ DEPENDENCIES mongoid (~> 2.0.0.rc.7) nokogiri rails (= 3.0.5) - rspec (~> 2.1) - rspec-rails (~> 2.1) + rspec (~> 2.5) + rspec-rails (~> 2.5) will_paginate From 74ed31b04b8d03c7f300b68f7b0fc8582bb05038 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 23 Mar 2011 14:01:09 +0300 Subject: [PATCH 065/115] Setting app id once - on create. --- app/models/app.rb | 2 +- spec/controllers/apps_controller_spec.rb | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/models/app.rb b/app/models/app.rb index e7b3fae8bf..073be685e2 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -10,7 +10,7 @@ class App # There seems to be a Mongoid bug making it impossible to use String identity with references_many feature: # https://github.com/mongoid/mongoid/issues/703 # Using 32 character string as a workaround. - before_save :on => :create do |r| + before_create do |r| r.id = ActiveSupport::SecureRandom.hex end diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 7eb420a74f..81468ddab4 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe AppsController do - + render_views + it_requires_authentication it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete} @@ -32,7 +33,6 @@ end describe "GET /apps/:id" do - render_views context 'logged in as an admin' do before(:each) do @user = Factory(:admin) @@ -151,10 +151,6 @@ end context "when the update is successful" do - before do - @app.should_receive(:update_attributes).and_return(true) - end - it "should redirect to the app page" do put :update, :id => @app.id, :app => {} response.should redirect_to(app_path(@app)) @@ -165,6 +161,14 @@ request.flash[:success].should match(/success/) end end + + context "changing name" do + it "should redirect to app page" do + id = @app.id + put :update, :id => id, :app => {:name => "new name"} + response.should redirect_to(app_path(id)) + end + end context "when the update is unsuccessful" do it "should render the edit page" do From 6b3dc1068a85143d2465296701879018676f4bd3 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 23 Mar 2011 14:08:19 +0300 Subject: [PATCH 066/115] Fixed haml deprecation warning. --- app/views/apps/_fields.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index 0a2c82031e..b96e78225d 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -10,7 +10,7 @@ %fieldset.nested-wrapper %legend Watchers - - f.fields_for :watchers do |w| + = f.fields_for :watchers do |w| %div.watcher.nested %div.choose = w.radio_button :watcher_type, :user @@ -20,4 +20,4 @@ %div.user{:class => w.object.email.blank? ? 'choosen' : nil} = w.select :user_id, User.all.map{|u| [u.name,u.id.to_s]}, :include_blank => '-- Select a User --' %div.email{:class => w.object.email.present? ? 'choosen' : nil} - = w.text_field :email \ No newline at end of file + = w.text_field :email From 35880a5676e417deab3087841ad218ed49603b09 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 29 Mar 2011 19:37:31 +0400 Subject: [PATCH 067/115] Lighthouseapp tracker model. --- Gemfile | 1 + Gemfile.lock | 4 ++ README.md | 5 +++ app/controllers/apps_controller.rb | 1 + app/models/app.rb | 14 +++++- app/models/issue_tracker.rb | 18 ++++++++ spec/controllers/apps_controller_spec.rb | 54 ++++++++++++++++++++++-- 7 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 app/models/issue_tracker.rb diff --git a/Gemfile b/Gemfile index 7f027659e1..f759356bd0 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gem 'mongoid', '~> 2.0.0.rc.7' gem 'haml' gem 'will_paginate' gem 'devise', '~> 1.1.8' +gem 'lighthouse-api' platform :ruby do gem 'bson_ext', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 85800ec0de..02f7520be3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,9 @@ GEM railties (>= 3.0.0) haml (3.0.25) i18n (0.5.0) + lighthouse-api (2.0) + activeresource (>= 3.0.0) + activesupport (>= 3.0.0) mail (2.2.15) activesupport (>= 2.3.6) i18n (>= 0.4.0) @@ -110,6 +113,7 @@ DEPENDENCIES devise (~> 1.1.8) factory_girl_rails haml + lighthouse-api mongoid (~> 2.0.0.rc.7) nokogiri rails (= 3.0.5) diff --git a/README.md b/README.md index 6557b7c969..1213b0112d 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,11 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at 4. Enjoy! +Lighthouseapp integration +------------------------- + +* Get an API token with full access to the project (visit http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token to learn how to get it) + TODO ---- diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 7b32f3b13f..e874380277 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -26,6 +26,7 @@ def new def edit @app.watchers.build if @app.watchers.none? + @app.issue_trackers.build if @app.issue_trackers.none? end def create diff --git a/app/models/app.rb b/app/models/app.rb index 073be685e2..b300438277 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -16,6 +16,7 @@ class App embeds_many :watchers embeds_many :deploys + embeds_many :issue_trackers references_many :errs, :dependent => :destroy before_validation :generate_api_key, :on => :create @@ -24,9 +25,12 @@ class App validates_uniqueness_of :name, :allow_blank => true validates_uniqueness_of :api_key, :allow_blank => true validates_associated :watchers + validate :check_issue_trackers accepts_nested_attributes_for :watchers, :allow_destroy => true, :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } + accepts_nested_attributes_for :issue_trackers, :allow_destroy => true, + :reject_if => proc { |attrs| !%w( lighthouseapp ).include?(attrs[:issue_tracker_type]) } # Mongoid Bug: find(id) on association proxies returns an Enumerator def self.find_by_id!(app_id) @@ -46,5 +50,13 @@ def last_deploy_at def generate_api_key self.api_key ||= ActiveSupport::SecureRandom.hex end - + + def check_issue_trackers + issue_trackers.map(&:valid?) + issue_trackers.each do |tracker| + tracker.errors.full_messages.each do |error| + errors[:base] << error + end if tracker.errors + end + end end diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb new file mode 100644 index 0000000000..ed7a4fe372 --- /dev/null +++ b/app/models/issue_tracker.rb @@ -0,0 +1,18 @@ +class IssueTracker + include Mongoid::Document + include Mongoid::Timestamps + + validate :check_lighthouseapp_params + + embedded_in :app, :inverse_of => :issue_trackers + + field :account, :type => String + field :api_token, :type => String + field :project_id, :type => String + field :issue_tracker_type, :type => String, :default => 'lighthouseapp' + + protected + def check_lighthouseapp_params + errors.add(:base, "You must specify your Lighthouseapp account, token and project id") if %w( api_token project_id account ).map {|m| self[m].blank? }.any? + end +end diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 81468ddab4..e9f6c3ebe1 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -147,7 +147,6 @@ describe "PUT /apps/:id" do before do @app = Factory(:app) - App.stub(:find).with(@app.id).and_return(@app) end context "when the update is successful" do @@ -172,11 +171,60 @@ context "when the update is unsuccessful" do it "should render the edit page" do - @app.should_receive(:update_attributes).and_return(false) - put :update, :id => @app.id, :app => {} + put :update, :id => @app.id, :app => { :name => '' } response.should render_template(:edit) end end + + context "setting up issue tracker", :cur => true do + context "unknown tracker type" do + before(:each) do + put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + :issue_tracker_type => 'unknown', :project_id => '1234', :api_token => '123123', :account => 'myapp' + } } } + @app.reload + end + + it "should not create issue tracker" do + @app.issue_trackers.should be_empty + end + end + + context "lighthouseapp" do + it "should save tracker params" do + put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123', :account => 'myapp' + } } } + @app.reload + + tracker = @app.issue_trackers.first + tracker.issue_tracker_type.should == 'lighthouseapp' + tracker.project_id.should == '1234' + tracker.api_token.should == '123123' + tracker.account.should == 'myapp' + end + + it "should show validation notice when sufficient params are not present" do + put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123' + } } } + @app.reload + + @app.issue_trackers.should be_empty + response.body.should match(/You must specify your Lighthouseapp account, token and project id/) + end + + it "should show validation notice when sufficient params are not present" do + put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123' + } } } + @app.reload + + @app.issue_trackers.should be_empty + response.body.should match(/You must specify your Lighthouseapp account, token and project id/) + end + end + end end describe "DELETE /apps/:id" do From 5d5d3b97ed88ec0c7bb09d18efbf756b29a05a4b Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 29 Mar 2011 19:43:51 +0400 Subject: [PATCH 068/115] Issue tracker setup UI. --- app/models/issue_tracker.rb | 5 ++++- app/views/apps/_fields.html.haml | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index ed7a4fe372..c969a134e7 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -13,6 +13,9 @@ class IssueTracker protected def check_lighthouseapp_params - errors.add(:base, "You must specify your Lighthouseapp account, token and project id") if %w( api_token project_id account ).map {|m| self[m].blank? }.any? + blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } + if blank_flags.any? && !blank_flags.all? + errors.add(:base, "You must specify your Lighthouseapp account, token and project id") + end end end diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index b96e78225d..2789a80efb 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -21,3 +21,20 @@ = w.select :user_id, User.all.map{|u| [u.name,u.id.to_s]}, :include_blank => '-- Select a User --' %div.email{:class => w.object.email.present? ? 'choosen' : nil} = w.text_field :email + +%fieldset.nested-wrapper + %legend Issue trackers + = f.fields_for :issue_trackers do |w| + %div.watcher.nested + %div.choose + = w.radio_button :issue_tracker_type, :lighthouseapp + = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp') + %div.lighthouseapp{:class => 'choosen'} + = w.label :account, "Account" + = w.text_field :account + = w.label :api_token, "API token" + = w.text_field :api_token + = w.label :project_id, "Project ID" + = w.text_field :project_id + + From e51f79752d92465885d0d120d50560f2ea0d7e0b Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 29 Mar 2011 19:48:42 +0400 Subject: [PATCH 069/115] Error messages layout fix. --- app/helpers/form_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index b10a2b0201..078fc7faad 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -6,7 +6,7 @@ def errors_for(document) content_tag(:div, :class => 'error-messages') do body = content_tag(:h2, 'Dang. The following errors are keeping this from being a success.') body += content_tag(:ul) do - document.errors.full_messages.inject('') {|errs, msg| errs += content_tag(:li, msg) } + document.errors.full_messages.inject('') {|errs, msg| errs += content_tag(:li, h(msg)) }.html_safe end end end From 5e0ed1f2e0bbaf080f477c710a697c281cdb27ef Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Tue, 29 Mar 2011 20:18:36 +0400 Subject: [PATCH 070/115] Minimal test for request to lighthouseapp. --- Gemfile | 1 + Gemfile.lock | 6 ++++++ app/models/issue_tracker.rb | 11 +++++++++++ spec/factories.rb | 1 + spec/factories/app_factories.rb | 2 +- spec/factories/issue_tracker_factories.rb | 7 +++++++ spec/models/issue_tracker_spec.rb | 17 +++++++++++++++++ spec/spec_helper.rb | 2 ++ 8 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 spec/factories/issue_tracker_factories.rb create mode 100644 spec/models/issue_tracker_spec.rb diff --git a/Gemfile b/Gemfile index f759356bd0..215773773a 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,7 @@ end group :development, :test do gem 'rspec-rails', '~> 2.5' + gem 'webmock' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 02f7520be3..4247bf2586 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -28,11 +28,13 @@ GEM activemodel (= 3.0.5) activesupport (= 3.0.5) activesupport (3.0.5) + addressable (2.2.4) arel (2.0.9) bcrypt-ruby (2.1.4) bson (1.2.4) bson_ext (1.2.4) builder (2.1.2) + crack (0.1.8) database_cleaner (0.6.5) devise (1.1.8) bcrypt-ruby (~> 2.1.2) @@ -102,6 +104,9 @@ GEM tzinfo (0.3.25) warden (1.0.3) rack (>= 1.0.0) + webmock (1.6.2) + addressable (>= 2.2.2) + crack (>= 0.1.7) will_paginate (3.0.pre2) PLATFORMS @@ -119,4 +124,5 @@ DEPENDENCIES rails (= 3.0.5) rspec (~> 2.5) rspec-rails (~> 2.5) + webmock will_paginate diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index c969a134e7..75025f0620 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -11,6 +11,17 @@ class IssueTracker field :project_id, :type => String field :issue_tracker_type, :type => String, :default => 'lighthouseapp' + def create_issue err + Lighthouse.account = account + Lighthouse.token = api_token + + ticket = Lighthouse::Ticket.new(:project_id => project_id) + ticket.title = "[#{ err.where }] #{err.message.to_s.truncate(27)}" + #ticket.body = err.backtrace.join("\n") + ticket.tags << "errbit" + ticket.save + end + protected def check_lighthouseapp_params blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } diff --git a/spec/factories.rb b/spec/factories.rb index 66fbae2390..f6a3dd4dc8 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,4 +1,5 @@ Factory.sequence(:name) {|n| "John #{n} Doe"} +Factory.sequence(:word) {|n| "word#{n}"} Factory.sequence(:app_name) {|n| "App ##{n}"} Factory.sequence(:email) {|n| "email#{n}@example.com"} Factory.sequence(:user_email) {|n| "user.#{n}@example.com"} diff --git a/spec/factories/app_factories.rb b/spec/factories/app_factories.rb index 5f655649ab..ed56a9898b 100644 --- a/spec/factories/app_factories.rb +++ b/spec/factories/app_factories.rb @@ -25,4 +25,4 @@ d.repository 'git@github.com/jdpace/errbit.git' d.environment 'production' d.revision ActiveSupport::SecureRandom.hex(10) -end \ No newline at end of file +end diff --git a/spec/factories/issue_tracker_factories.rb b/spec/factories/issue_tracker_factories.rb new file mode 100644 index 0000000000..b31eaae760 --- /dev/null +++ b/spec/factories/issue_tracker_factories.rb @@ -0,0 +1,7 @@ +Factory.define :lighthouseapp_tracker, :class => IssueTracker do |e| + e.issue_tracker_type 'lighthouseapp' + e.account { Factory.next :word } + e.api_token { Factory.next :word } + e.project_id { Factory.next :word } + e.association :app, :factory => :app +end \ No newline at end of file diff --git a/spec/models/issue_tracker_spec.rb b/spec/models/issue_tracker_spec.rb new file mode 100644 index 0000000000..9b7e8c7098 --- /dev/null +++ b/spec/models/issue_tracker_spec.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 +require 'spec_helper' + +describe IssueTracker do + describe "#create_issue" do + context "lighthouseapp tracker" do + let(:tracker) { Factory :lighthouseapp_tracker } + let(:err) { Factory :err } + + it "should make request to Lighthouseapp with err params" do + stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") + tracker.create_issue err + WebMock.should have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a190415d07..f4409ab3bc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'database_cleaner' +require 'webmock/rspec' # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. @@ -21,4 +22,5 @@ DatabaseCleaner[:mongoid].strategy = :truncation DatabaseCleaner.clean end + config.include WebMock::API end \ No newline at end of file From 12640a498a255d9adf49a9479c2d07af5f2b2703 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 30 Mar 2011 15:42:23 +0400 Subject: [PATCH 071/115] One issue tracker per app is enough. --- app/controllers/apps_controller.rb | 2 +- app/models/app.rb | 16 ++++++++-------- app/models/issue_tracker.rb | 2 +- app/views/apps/_fields.html.haml | 4 ++-- spec/controllers/apps_controller_spec.rb | 24 ++++++++++++------------ 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index e874380277..27449b80d6 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -26,7 +26,7 @@ def new def edit @app.watchers.build if @app.watchers.none? - @app.issue_trackers.build if @app.issue_trackers.none? + @app.issue_tracker = IssueTracker.new if @app.issue_tracker.nil? end def create diff --git a/app/models/app.rb b/app/models/app.rb index b300438277..b9601216ea 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -16,7 +16,7 @@ class App embeds_many :watchers embeds_many :deploys - embeds_many :issue_trackers + embeds_one :issue_tracker references_many :errs, :dependent => :destroy before_validation :generate_api_key, :on => :create @@ -25,11 +25,11 @@ class App validates_uniqueness_of :name, :allow_blank => true validates_uniqueness_of :api_key, :allow_blank => true validates_associated :watchers - validate :check_issue_trackers + validate :check_issue_tracker accepts_nested_attributes_for :watchers, :allow_destroy => true, :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } - accepts_nested_attributes_for :issue_trackers, :allow_destroy => true, + accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, :reject_if => proc { |attrs| !%w( lighthouseapp ).include?(attrs[:issue_tracker_type]) } # Mongoid Bug: find(id) on association proxies returns an Enumerator @@ -51,12 +51,12 @@ def generate_api_key self.api_key ||= ActiveSupport::SecureRandom.hex end - def check_issue_trackers - issue_trackers.map(&:valid?) - issue_trackers.each do |tracker| - tracker.errors.full_messages.each do |error| + def check_issue_tracker + if issue_tracker.present? + issue_tracker.valid? + issue_tracker.errors.full_messages.each do |error| errors[:base] << error - end if tracker.errors + end if issue_tracker.errors end end end diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index 75025f0620..8242fe3000 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -4,7 +4,7 @@ class IssueTracker validate :check_lighthouseapp_params - embedded_in :app, :inverse_of => :issue_trackers + embedded_in :app, :inverse_of => :issue_tracker field :account, :type => String field :api_token, :type => String diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index 2789a80efb..ef927d27ec 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -23,8 +23,8 @@ = w.text_field :email %fieldset.nested-wrapper - %legend Issue trackers - = f.fields_for :issue_trackers do |w| + %legend Issue tracker + = f.fields_for :issue_tracker do |w| %div.watcher.nested %div.choose = w.radio_button :issue_tracker_type, :lighthouseapp diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index e9f6c3ebe1..d477eab25d 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -179,25 +179,25 @@ context "setting up issue tracker", :cur => true do context "unknown tracker type" do before(:each) do - put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { :issue_tracker_type => 'unknown', :project_id => '1234', :api_token => '123123', :account => 'myapp' - } } } + } } @app.reload end it "should not create issue tracker" do - @app.issue_trackers.should be_empty + @app.issue_tracker.should be_nil end end context "lighthouseapp" do it "should save tracker params" do - put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123', :account => 'myapp' - } } } + } } @app.reload - tracker = @app.issue_trackers.first + tracker = @app.issue_tracker tracker.issue_tracker_type.should == 'lighthouseapp' tracker.project_id.should == '1234' tracker.api_token.should == '123123' @@ -205,22 +205,22 @@ end it "should show validation notice when sufficient params are not present" do - put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123' - } } } + } } @app.reload - @app.issue_trackers.should be_empty + @app.issue_tracker.should be_nil response.body.should match(/You must specify your Lighthouseapp account, token and project id/) end it "should show validation notice when sufficient params are not present" do - put :update, :id => @app.id, :app => { :issue_trackers_attributes => { '0' => { + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123' - } } } + } } @app.reload - @app.issue_trackers.should be_empty + @app.issue_tracker.should be_nil response.body.should match(/You must specify your Lighthouseapp account, token and project id/) end end From 30b00acfc34c1db93d3f8dc1460452ea866ed0c0 Mon Sep 17 00:00:00 2001 From: Nick Recobra Date: Wed, 30 Mar 2011 18:50:27 +0400 Subject: [PATCH 072/115] Specs for lighthouse tracker integration. --- Gemfile | 2 +- app/controllers/apps_controller.rb | 1 + app/controllers/errs_controller.rb | 20 ++++- app/models/err.rb | 1 + app/models/issue_tracker.rb | 70 ++++++++++++++- app/views/errs/show.html.haml | 8 +- config/environments/development.rb | 1 + config/environments/test.rb | 1 + config/routes.rb | 1 + spec/controllers/errs_controller_spec.rb | 107 ++++++++++++++++++++++- spec/models/issue_tracker_spec.rb | 12 --- 11 files changed, 203 insertions(+), 21 deletions(-) diff --git a/Gemfile b/Gemfile index 215773773a..f005b2ac8b 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,7 @@ end group :development, :test do gem 'rspec-rails', '~> 2.5' - gem 'webmock' + gem 'webmock', :require => false end group :test do diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 27449b80d6..3bf60825aa 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -22,6 +22,7 @@ def show def new @app = App.new @app.watchers.build + @app.issue_tracker = IssueTracker.new end def edit diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index e7ca172395..d4aa55d262 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -1,6 +1,7 @@ class ErrsController < ApplicationController before_filter :find_app, :except => [:index, :all] + before_filter :find_err, :except => [:index, :all] def index app_scope = current_user.admin? ? App.all : current_user.apps @@ -20,16 +21,25 @@ def all end def show - @err = @app.errs.find(params[:id]) page = (params[:notice] || @err.notices.count) page = 1 if page.to_i.zero? @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1) @notice = @notices.first end + + def create_issue + if @app.issue_tracker + @app.issue_tracker.create_issue @err + else + flash[:error] = "This up has no issue tracker setup." + end + redirect_to app_err_path(@app, @err) + rescue ActiveResource::ConnectionError + flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later." + redirect_to app_err_path(@app, @err) + end def resolve - @err = @app.errs.find(params[:id]) - # Deal with bug in mogoid where find is returning an Enumberable obj @err = @err.first if @err.respond_to?(:first) @@ -51,5 +61,9 @@ def find_app # apparently finding by 'watchers.email' and 'id' is broken raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) end + + def find_err + @err = @app.errs.find(params[:id]) + end end diff --git a/app/models/err.rb b/app/models/err.rb index d38f1ebeb7..9f7e3388c1 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -9,6 +9,7 @@ class Err field :fingerprint field :last_notice_at, :type => DateTime field :resolved, :type => Boolean, :default => false + field :issue_link, :type => String index :last_notice_at diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index 8242fe3000..3ce63602be 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -1,6 +1,9 @@ class IssueTracker include Mongoid::Document include Mongoid::Timestamps + include HashHelper + include Rails.application.routes.url_helpers + default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host] validate :check_lighthouseapp_params @@ -15,11 +18,72 @@ def create_issue err Lighthouse.account = account Lighthouse.token = api_token + # updating lighthouse account + Lighthouse::Ticket.site + ticket = Lighthouse::Ticket.new(:project_id => project_id) - ticket.title = "[#{ err.where }] #{err.message.to_s.truncate(27)}" - #ticket.body = err.backtrace.join("\n") + ticket.title = "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" + + ticket.body = "" + ticket.body += "[See this exception on Errbit](#{ app_err_url err.app, err } \"See this exception on Errbit\")" + ticket.body += "\n" + if notice = err.notices.first + ticket.body += "# #{notice.message} #" + ticket.body += "\n" + ticket.body += "## Summary ##" + ticket.body += "\n" + if notice.request['url'].present? + ticket.body += "### URL ###" + ticket.body += "\n" + ticket.body += "[#{notice.request['url']}](#{notice.request['url']})" + ticket.body += "\n" + end + ticket.body += "### Where ###" + ticket.body += "\n" + ticket.body += notice.err.where + ticket.body += "\n" + + ticket.body += "### Occured ###" + ticket.body += "\n" + ticket.body += notice.created_at.to_s(:micro) + ticket.body += "\n" + + ticket.body += "### Similar ###" + ticket.body += "\n" + ticket.body += (notice.err.notices.count - 1).to_s + ticket.body += "\n" + + ticket.body += "## Params ##" + ticket.body += "\n" + ticket.body += "#{pretty_hash(notice.params)}" + ticket.body += "\n" + + ticket.body += "## Session ##" + ticket.body += "\n" + ticket.body += "#{pretty_hash(notice.session)}" + ticket.body += "\n" + + ticket.body += "## Backtrace ##" + ticket.body += "\n" + ticket.body += "" + for line in notice.backtrace + ticket.body += "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> **#{line['method']}**" + ticket.body += "\n" + end + ticket.body += "" + ticket.body += "\n" + + ticket.body += "## Environment ##" + ticket.body += "\n" + for key, val in notice.env_vars + ticket.body += "#{key}: #{val}" + end + ticket.body += "\n" + end + ticket.tags << "errbit" - ticket.save + ticket.save! + err.update_attribute :issue_link, "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, '') end protected diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index 11b4fb928d..475c0e1ac7 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -10,7 +10,13 @@ %strong Last Notice: = last_notice_at(@err).to_s(:micro) - content_for :action_bar do - %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' if @err.unresolved? + - if @err.unresolved? + - if @err.app.issue_tracker + - if @err.issue_link.blank? + %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => 'create-issue' + - else + %span= link_to 'go to issue', @err.issue_link, :class => 'goto-issue' + %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' %h4= @notice.try(:message) diff --git a/config/environments/development.rb b/config/environments/development.rb index 666269148c..4a01584d4b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,6 +16,7 @@ # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false + config.action_mailer.default_url_options = { :host => 'localhost:3000' } # Print deprecation notices to the Rails logger config.active_support.deprecation = :log diff --git a/config/environments/test.rb b/config/environments/test.rb index 9dfc650c05..1922d219a8 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -24,6 +24,7 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { :host => 'test.host' } # Use SQL instead of Active Record's schema dumper when creating the test database. # This is necessary if your schema can't be completely dumped by the schema dumper, diff --git a/config/routes.rb b/config/routes.rb index 33afb06a2f..a29d8e9b4f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,6 +20,7 @@ resources :notices member do put :resolve + post :create_issue end end diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 3952604334..017bd80c38 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -123,6 +123,33 @@ get :show, :app_id => app.id, :id => err.id response.should be_success end + + context "create issue button" do + let(:button_matcher) { match(/create issue/) } + + it "should not exist for err's app without issue tracker" do + err = Factory :err + get :show, :app_id => err.app.id, :id => err.id + + response.body.should_not button_matcher + end + + it "should exist for err's app with issue tracker" do + tracker = Factory(:lighthouseapp_tracker) + err = Factory(:err, :app => tracker.app) + get :show, :app_id => err.app.id, :id => err.id + + response.body.should button_matcher + end + + it "should not exist for err with issue_link" do + tracker = Factory(:lighthouseapp_tracker) + err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host") + get :show, :app_id => err.app.id, :id => err.id + + response.body.should_not button_matcher + end + end end context 'when logged in as a user' do @@ -186,5 +213,83 @@ response.should redirect_to(errs_path) end end - + + describe "POST /apps/:app_id/errs/:id/create_issue" do + render_views + + before(:each) do + sign_in Factory(:admin) + end + + context "successful issue creation" do + context "lighthouseapp tracker" do + let(:notice) { Factory :notice } + let(:tracker) { Factory :lighthouseapp_tracker, :app => notice.err.app } + let(:err) { notice.err } + + before(:each) do + number = 5 + @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml" + body = "#{number}" + stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) + + post :create_issue, :app_id => err.app.id, :id => err.id + err.reload + end + + it "should make request to Lighthouseapp with err params" do + requested = have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") + WebMock.should requested.with(:headers => {'X-Lighthousetoken' => tracker.api_token}) + WebMock.should requested.with(:body => /errbit<\/tag>/) + WebMock.should requested.with(:body => /\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/title>/) + WebMock.should requested.with(:body => /<body>.+<\/body>/m) + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + + it "should create issue link for err" do + err.issue_link.should == @issue_link.sub(/\.xml$/, '') + end + end + end + + context "absent issue tracker" do + let(:err) { Factory :err } + + before(:each) do + post :create_issue, :app_id => err.app.id, :id => err.id + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + + it "should set flash error message telling issue tracker of the app doesn't exist" do + flash[:error].should == "This up has no issue tracker setup." + end + end + + context "error during request to a tracker" do + context "lighthouseapp tracker" do + let(:tracker) { Factory :lighthouseapp_tracker } + let(:err) { Factory :err, :app => tracker.app } + + before(:each) do + stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").to_return(:status => 500) + + post :create_issue, :app_id => err.app.id, :id => err.id + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + + it "should notify of connection error" do + flash[:error].should == "There was an error during issue creation. Check your tracker settings or try again later." + end + end + end + end end diff --git a/spec/models/issue_tracker_spec.rb b/spec/models/issue_tracker_spec.rb index 9b7e8c7098..6b5f577724 100644 --- a/spec/models/issue_tracker_spec.rb +++ b/spec/models/issue_tracker_spec.rb @@ -2,16 +2,4 @@ require 'spec_helper' describe IssueTracker do - describe "#create_issue" do - context "lighthouseapp tracker" do - let(:tracker) { Factory :lighthouseapp_tracker } - let(:err) { Factory :err } - - it "should make request to Lighthouseapp with err params" do - stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") - tracker.create_issue err - WebMock.should have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") - end - end - end end From 697bee6aa53fd2f07e27adeed923b8b3ac1e7cbd Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 30 Mar 2011 19:02:59 +0400 Subject: [PATCH 073/115] Setting IssueTracker url options on request. --- app/controllers/errs_controller.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index d4aa55d262..268d0e79b7 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -28,6 +28,8 @@ def show end def create_issue + set_tracker_params + if @app.issue_tracker @app.issue_tracker.create_issue @err else @@ -65,5 +67,11 @@ def find_app def find_err @err = @app.errs.find(params[:id]) end + + def set_tracker_params + IssueTracker.default_url_options[:host] = request.host + IssueTracker.default_url_options[:port] = request.port + IssueTracker.default_url_options[:protocol] = request.scheme + end end From c1c364f50eae009527969e35fe675dd6fa9ebab6 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 30 Mar 2011 19:42:23 +0400 Subject: [PATCH 074/115] Ability to clear err issue link. --- app/controllers/errs_controller.rb | 5 ++++ app/views/errs/show.html.haml | 11 ++++---- config/routes.rb | 1 + spec/controllers/errs_controller_spec.rb | 36 ++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index 268d0e79b7..d09074e18f 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -40,6 +40,11 @@ def create_issue flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later." redirect_to app_err_path(@app, @err) end + + def clear_issue + @err.update_attribute :issue_link, nil + redirect_to app_err_path(@app, @err) + end def resolve # Deal with bug in mogoid where find is returning an Enumberable obj diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index 475c0e1ac7..78223109d0 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -10,12 +10,13 @@ %strong Last Notice: = last_notice_at(@err).to_s(:micro) - content_for :action_bar do + - if @err.app.issue_tracker + - if @err.issue_link.blank? + %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => 'create-issue' + - else + %span= link_to 'go to issue', @err.issue_link, :class => 'goto-issue' + = link_to 'clear issue', clear_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Clear err issues?", :class => 'clear-issue' - if @err.unresolved? - - if @err.app.issue_tracker - - if @err.issue_link.blank? - %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => 'create-issue' - - else - %span= link_to 'go to issue', @err.issue_link, :class => 'goto-issue' %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' %h4= @notice.try(:message) diff --git a/config/routes.rb b/config/routes.rb index a29d8e9b4f..63dbd20925 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,6 +21,7 @@ member do put :resolve post :create_issue + delete :clear_issue end end diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 017bd80c38..4163bd306c 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -292,4 +292,40 @@ end end end + + describe "DELETE /apps/:app_id/errs/:id/clear_issue" do + before(:each) do + sign_in Factory(:admin) + end + + context "err with issue" do + let(:err) { Factory :err, :issue_link => "http://some.host" } + + before(:each) do + delete :clear_issue, :app_id => err.app.id, :id => err.id + err.reload + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + + it "should clear issue link" do + err.issue_link.should be_nil + end + end + + context "err without issue" do + let(:err) { Factory :err } + + before(:each) do + delete :clear_issue, :app_id => err.app.id, :id => err.id + err.reload + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + end + end end From 3b817dd9cda843fc717bce9e903a0e0995cc739e Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 30 Mar 2011 19:45:52 +0400 Subject: [PATCH 075/115] Single issue tracker form. --- app/views/apps/_fields.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index ef927d27ec..2d3b763b6d 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -22,7 +22,7 @@ %div.email{:class => w.object.email.present? ? 'choosen' : nil} = w.text_field :email -%fieldset.nested-wrapper +%fieldset %legend Issue tracker = f.fields_for :issue_tracker do |w| %div.watcher.nested From 1812e0552399e8615ecfd731b7f77091c984d6b7 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 30 Mar 2011 19:54:13 +0400 Subject: [PATCH 076/115] Added Lighthouse integration section to the readme. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1213b0112d..d5527417da 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,9 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at Lighthouseapp integration ------------------------- -* Get an API token with full access to the project (visit http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token to learn how to get it) +* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview +* Errbit uses token-based authentication. Get your API Token or visit http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token to learn how to get it. +* Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview TODO ---- From 3960fb5d66fa9627695dbbcafd5ff9293ccb2727 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 30 Mar 2011 19:56:01 +0400 Subject: [PATCH 077/115] Link to lighthouse knowledge base. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5527417da..9bf384c9b9 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Lighthouseapp integration ------------------------- * Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview -* Errbit uses token-based authentication. Get your API Token or visit http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token to learn how to get it. +* Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it. * Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview TODO From f618448d9e119fad0f487db968e4e14d419e28d6 Mon Sep 17 00:00:00 2001 From: Raja Bhadury <rajabhadury@gmail.com> Date: Thu, 31 Mar 2011 09:31:04 +0100 Subject: [PATCH 078/115] Include 'message' in deploy notice, add tests for app err/deploy notification switches. --- spec/controllers/deploys_controller_spec.rb | 7 +++++-- spec/models/deploy_spec.rb | 7 +++++++ spec/models/err_spec.rb | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/spec/controllers/deploys_controller_spec.rb b/spec/controllers/deploys_controller_spec.rb index 9578ae390a..72519c4d9d 100644 --- a/spec/controllers/deploys_controller_spec.rb +++ b/spec/controllers/deploys_controller_spec.rb @@ -9,7 +9,8 @@ 'local_username' => 'john.doe', 'scm_repository' => 'git@github.com/jdpace/errbit.git', 'rails_env' => 'production', - 'scm_revision' => '19d77837eef37902cf5df7e4445c85f392a8d0d5' + 'scm_revision' => '19d77837eef37902cf5df7e4445c85f392a8d0d5', + 'message' => 'johns first deploy' } @app = Factory(:app_with_watcher, :api_key => 'APIKEY') end @@ -26,7 +27,9 @@ :username => 'john.doe', :environment => 'production', :repository => 'git@github.com/jdpace/errbit.git', - :revision => '19d77837eef37902cf5df7e4445c85f392a8d0d5' + :revision => '19d77837eef37902cf5df7e4445c85f392a8d0d5', + :message => 'johns first deploy' + }).and_return(Factory(:deploy)) post :create, :deploy => @params, :api_key => 'APIKEY' end diff --git a/spec/models/deploy_spec.rb b/spec/models/deploy_spec.rb index a82c14eccb..9ae5f7ead0 100644 --- a/spec/models/deploy_spec.rb +++ b/spec/models/deploy_spec.rb @@ -42,6 +42,13 @@ @staging_errs.all?{|err| err.reload.resolved?}.should == false end end + + context 'when the app has deploy notifications set to false' do + it 'should not send an email notification' do + Mailer.should_not_receive(:deploy_notification) + Factory(:deploy, :app => Factory(:app_with_watcher, :notify_on_deploys => false)) + end + end end end diff --git a/spec/models/err_spec.rb b/spec/models/err_spec.rb index 4410200efb..7647b42df6 100644 --- a/spec/models/err_spec.rb +++ b/spec/models/err_spec.rb @@ -120,5 +120,14 @@ end end end - + + context 'being created' do + context 'when the app has err notifications set to false' do + it 'should not send an email notification' do + app = Factory(:app_with_watcher, :notify_on_deploys => false) + Mailer.should_not_receive(:err_notification) + Factory(:err, :app => app) + end + end + end end \ No newline at end of file From 5e84da65af95c6450c64124ac4a53dd9848b0cfd Mon Sep 17 00:00:00 2001 From: Raja Bhadury <rajabhadury@gmail.com> Date: Thu, 31 Mar 2011 09:37:11 +0100 Subject: [PATCH 079/115] Ooops, should be err_notificaiton not deploy _notification --- spec/models/err_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/err_spec.rb b/spec/models/err_spec.rb index 7647b42df6..95f68e90dd 100644 --- a/spec/models/err_spec.rb +++ b/spec/models/err_spec.rb @@ -124,7 +124,7 @@ context 'being created' do context 'when the app has err notifications set to false' do it 'should not send an email notification' do - app = Factory(:app_with_watcher, :notify_on_deploys => false) + app = Factory(:app_with_watcher, :notify_on_errs => false) Mailer.should_not_receive(:err_notification) Factory(:err, :app => app) end From 8771d6c26f91b031d018fe521c90373a7e4e6a74 Mon Sep 17 00:00:00 2001 From: Raja Bhadury <rajabhadury@gmail.com> Date: Fri, 1 Apr 2011 08:58:01 +0100 Subject: [PATCH 080/115] Default errbit to send notification emails on deploys and errors --- app/models/app.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/app.rb b/app/models/app.rb index 28ce850bce..a9263512fb 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -5,8 +5,8 @@ class App field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false - field :notify_on_errs, :type => Boolean, :default => false - field :notify_on_deploys, :type => Boolean, :default => false + field :notify_on_errs, :type => Boolean, :default => true + field :notify_on_deploys, :type => Boolean, :default => true key :name embeds_many :watchers From fb3dd19efbc19665e57c832c8bf7e5b07464b398 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 1 Apr 2011 12:52:54 +0400 Subject: [PATCH 081/115] Redmine issue tracker setup. --- app/helpers/application_helper.rb | 3 +++ app/models/app.rb | 2 +- app/models/issue_tracker.rb | 11 ++++++++--- app/views/apps/_fields.html.haml | 17 +++++++++++++---- public/javascripts/form.js | 19 +++++++++++++++++++ public/stylesheets/application.css | 9 +++++---- spec/controllers/apps_controller_spec.rb | 21 ++++++++++++++++++--- 7 files changed, 67 insertions(+), 15 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be7945c..a5ba883fe8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,5 @@ module ApplicationHelper + def lighthouse_tracker? object + object.issue_tracker_type == "lighthouseapp" + end end diff --git a/app/models/app.rb b/app/models/app.rb index b9601216ea..9550be4e84 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -30,7 +30,7 @@ class App accepts_nested_attributes_for :watchers, :allow_destroy => true, :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, - :reject_if => proc { |attrs| !%w( lighthouseapp ).include?(attrs[:issue_tracker_type]) } + :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) } # Mongoid Bug: find(id) on association proxies returns an Enumerator def self.find_by_id!(app_id) diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index 3ce63602be..a770c570f8 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -5,7 +5,7 @@ class IssueTracker include Rails.application.routes.url_helpers default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host] - validate :check_lighthouseapp_params + validate :check_params embedded_in :app, :inverse_of => :issue_tracker @@ -87,10 +87,15 @@ def create_issue err end protected - def check_lighthouseapp_params + def check_params blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } if blank_flags.any? && !blank_flags.all? - errors.add(:base, "You must specify your Lighthouseapp account, token and project id") + message = if issue_tracker_type == 'lighthouseapp' + "You must specify your Lighthouseapp account, api token and project id" + else + "You must specify your Redmine url, api token and project id" + end + errors.add(:base, message) end end end diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index 2d3b763b6d..99ba854ee8 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -25,15 +25,24 @@ %fieldset %legend Issue tracker = f.fields_for :issue_tracker do |w| - %div.watcher.nested + %div.issue_tracker.nested %div.choose = w.radio_button :issue_tracker_type, :lighthouseapp = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp') - %div.lighthouseapp{:class => 'choosen'} + = w.radio_button :issue_tracker_type, :redmine + = label_tag :issue_tracker_type_redmine, 'Redmine', :for => label_for_attr(w, 'issue_tracker_type_redmine') + %div.tracker_params{:class => lighthouse_tracker?(w.object) ? 'choosen' : nil} = w.label :account, "Account" - = w.text_field :account + = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" + = w.label :api_token, "API token" + = w.text_field :api_token, :placeholder => "API Token for your account" + = w.label :project_id, "Project ID" + = w.text_field :project_id, :placeholder => "123 from abc from abc.lighthouseapp.com/projects/123" + %div.tracker_params{:class => lighthouse_tracker?(w.object) ? nil : 'choosen'} + = w.label :account, "Redmine URL" + = w.text_field :account, :placeholder => "like http://www.redmine.org/" = w.label :api_token, "API token" - = w.text_field :api_token + = w.text_field :api_token, :placeholder => "API Token for your account" = w.label :project_id, "Project ID" = w.text_field :project_id diff --git a/public/javascripts/form.js b/public/javascripts/form.js index a7b6aeb89e..b2e1eefef8 100644 --- a/public/javascripts/form.js +++ b/public/javascripts/form.js @@ -3,6 +3,9 @@ $(function(){ if($('div.watcher.nested').length) activateWatcherTypeSelector(); + + if($('div.issue_tracker.nested').length) + activateIssueTrackerTypeSelector(); }); function activateNestedForms() { @@ -67,4 +70,20 @@ function activateWatcherTypeSelector() { wrapper.find('div.choosen').removeClass('choosen'); wrapper.find('div.'+choosen).addClass('choosen'); }); +} + +function activateIssueTrackerTypeSelector() { + var not_choosen = $("div.tracker_params").filter(function () { + return !$(this).hasClass("choosen"); + }); + window.hiddenTracker = not_choosen.html(); + not_choosen.remove(); + $('div.issue_tracker input[name*=issue_tracker_type]').live('click', function(){ + var choosen = $(this).val(); + var wrapper = $(this).closest('.nested'); + var tmp; + tmp = wrapper.find('div.choosen').html(); + wrapper.find('div.choosen').html(window.hiddenTracker); + window.hiddenTracker = tmp; + }); } \ No newline at end of file diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index e6e480944b..628d807d4c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -501,14 +501,15 @@ a.button.active { margin-right: 14px; } -/* Watchers Form */ -div.nested.watcher .user, div.nested.watcher .email { +/* Watchers and Issue Tracker Forms */ +div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .lighthouseapp, div.issue_tracker.nested .redmine { display: none; } -div.nested.watcher .choosen { +div.nested.watcher .choosen, div.nested.issue_tracker .choosen { display: block; } -div.nested.watcher .choose { + +div.nested.watcher .choose, div.nested.issue_tracker .choose { margin-bottom: 0.5em; } diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index d477eab25d..38eb9c7e30 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -211,17 +211,32 @@ @app.reload @app.issue_tracker.should be_nil - response.body.should match(/You must specify your Lighthouseapp account, token and project id/) + response.body.should match(/You must specify your Lighthouseapp account, api token and project id/) + end + end + + context "redmine" do + it "should save tracker params" do + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { + :issue_tracker_type => 'redmine', :project_id => '1234', :api_token => '123123', :account => 'http://myapp.com' + } } + @app.reload + + tracker = @app.issue_tracker + tracker.issue_tracker_type.should == 'redmine' + tracker.project_id.should == '1234' + tracker.api_token.should == '123123' + tracker.account.should == 'http://myapp.com' end it "should show validation notice when sufficient params are not present" do put :update, :id => @app.id, :app => { :issue_tracker_attributes => { - :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123' + :issue_tracker_type => 'redmine', :project_id => '1234', :api_token => '123123' } } @app.reload @app.issue_tracker.should be_nil - response.body.should match(/You must specify your Lighthouseapp account, token and project id/) + response.body.should match(/You must specify your Redmine url, api token and project id/) end end end From 93b6d0a5efd0dcf54deea903787dd82ab7e06cd0 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 1 Apr 2011 13:08:54 +0400 Subject: [PATCH 082/115] Icons for issue trackers. --- app/views/errs/show.html.haml | 6 +++--- public/images/lighthouseapp_create.png | Bin 0 -> 1605 bytes public/images/lighthousehouseapp_goto.png | Bin 0 -> 1651 bytes public/images/redmine_create.png | Bin 0 -> 1510 bytes public/images/redmine_goto.png | Bin 0 -> 1555 bytes public/stylesheets/application.css | 16 ++++++++++++++++ 6 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 public/images/lighthouseapp_create.png create mode 100644 public/images/lighthousehouseapp_goto.png create mode 100644 public/images/redmine_create.png create mode 100644 public/images/redmine_goto.png diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index 78223109d0..ade5c90cd1 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -12,10 +12,10 @@ - content_for :action_bar do - if @err.app.issue_tracker - if @err.issue_link.blank? - %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => 'create-issue' + %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => "#{@app.issue_tracker.issue_tracker_type}_create create-issue" - else - %span= link_to 'go to issue', @err.issue_link, :class => 'goto-issue' - = link_to 'clear issue', clear_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Clear err issues?", :class => 'clear-issue' + %span= link_to 'go to issue', @err.issue_link, :class => "#{@app.issue_tracker.issue_tracker_type}_create goto-issue" + = link_to 'clear issue', clear_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Clear err issues?", :class => "clear-issue" - if @err.unresolved? %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' diff --git a/public/images/lighthouseapp_create.png b/public/images/lighthouseapp_create.png new file mode 100644 index 0000000000000000000000000000000000000000..a28b9a0c334f8cd53f4b25fce3540b270ad35b43 GIT binary patch literal 1605 zcmV-L2D<r)P)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%=Sf6CRCwCNR()tyWf=e6@9*8a>vlJ< zE}cc&TFp&`eV}a$CImrIP}pA<Mv#SIMxY3l|5>oKAo?eRf(%6?=tE;xTbr}g8E#Hf zR%@<p_wL@^k8{88xjoOjox6LyPS*$c!jE$}=Xsys`##^ATrP*Zv9!2LOTSpb{{H@I zdOS_oOmGq!=Y9<kLWEO<BhAgtBa6|exs$2SoH?_e9{Y7VUAxt41%uvUGMU8xWHO0N zCWBZkhIl-VY&QD?eSWQ_rRD6uA|^s3J>D@G3_D6nO5k)lq1Wr7)oO8m#E-5^W2kUi z(NVt)%iUHc6bA@}LWo2n{QfTO-$e(=E}FQnug^`7<956KVUNcHA{SuNFVFpsqg}rs zJNOk~us~P;D7I~GMEe6?LCjz)`2Bvi!gr+Ssn*uk&>e}1)S}U7x*ZP3S|&9bjpC+R z5<7Hi7(HDlVZQM*Ft;zK)gfNpfsGs5@XF>|6}ivnWAZ-Y=pgdcouc{>`7uw*Hw`2; z*m-OiKb<%XO981kxtxW4qzk9NID%JCTt_ga6?-@a%F4=MHk(^X?Csm8Q*UqYS_+)! z3(b3NM#dYbr{Rm`pf}sl*jNLX%aPw>G#XG>yNXPg!JnZd_Wu~fNGK<)#0M`eEv0kj zULxI%3#JqK@O?6|*u?O~b2!)+L@X`CR^rCAcMbOKS$E4|Y}t1nt4hlurDPmB8^WFq zE;#i%K~BM`l_aT?#5#&@+|$#;Cu(-NT>0qz=t5YK7Orgg55xvOK=9i43#JN_e|#}2 z(vEd;R#4Qp6URM8#N>+?l^kW#OH*lF_0PvcGA@Z{CU6;Y+=r=)M~euPF)9Ckh4x+z zWQ8sU`<yto77>#lHqno~VE>JzN@+MSg}slIKwtJCbhb*A-nUWp|E(=nAyc?6Os4bt zShp<8&lC}-)9GdG4;G?y!mvN3HeV=+^$km*voD9GVLLplpH%(dw5|$~pkMWE$d?wn zSSJT6nR``Ha8d4ASU}0^V7$2COq`oFuioN>MmM`PN9mM_&CE%xR>@DtvWhhYIT2fK znNBRAK(E!Pl$0#-P-@v>)EOwk(#R&Fga|bBZq%Fb8~tOMo0p<bN&BK3Q}ku%L<U9B zY1ZaX%**O<;du81>OCh>v3qb{aWVq`fqEPXZ-u?ats-4w)v;fNKCa~yxS66GQ`AXZ z=2(DHQ)W^rNhcFnj=$y$M|XvQvR35AkV?c=d+I$VLCj#?#1Snbrc@f`*2Z;~iPw5> zCXS;@GdW)>6S9D^sx%WwCDp`hmKuaUCgzq&9qvkzsoie(H&oOcl=k)z-S}R&5!VAL zSPfd7+|^D}r{{^KSvP1*4wUa5#k2JOcrt_S$8U)5_tXAry9`O1)yKqCK0hnEaYaSN zVY+oJ5C|ZZN(sT|);I-e-YTEE^3Y1MK;btfh<MMoCWkT@h#pPVOQOpiyH!+QuCA^= z#X^+R0xn$=J6hZ@>1TscL&0`iaG@aCoo#Md<(SaHzU9lTT)FaVQZc?zKdu`b9PAB; z!>-E8N>Si#HXA~+87tgomH+IWHt~$^oG+7!Ob!#F1nfCUj6zI2H8n-u%r#wCSJy@& zR~PEXmr0<Ll07y#IVnsYjYeV3MvzD(aBfV(hy8w4aK)*Kyw~f)`LQT+=>#m9aGsP0 z0jgM1O-;>#Lehms9~}{XGB7ak7zKr`yu4i8oSO&NTV>#Lyg6%u$i9R)9Q#x8DU<T( zL$yI_YHIqN$hUK2Tg<TMpzobSLqkuHsY<!6al2%4Mpac+UXW8|baYe%7QfHT%z#`4 za#f(Vw)O<o>X%6@_D{xJ9pUw0F!;jo@NhZZ%EJC;aF6ApKi6DDx=^DRpaC&Xgg#20 zgY>)e=9?~huBO#M*iC+G8y_EckO^%>rX{oT_zR20lA=S%G+#+xuQx(&=%vr>(_jD7 zKwq%Dge`;(1Q$V>CPoR|p1KL&7N*SqMf|_6zXcco^BokukuskJ00000NkvXXu0mjf DB$5Ys literal 0 HcmV?d00001 diff --git a/public/images/lighthousehouseapp_goto.png b/public/images/lighthousehouseapp_goto.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e468b94b55cfcd097fbff91a694f156ff34992 GIT binary patch literal 1651 zcmV-(28{WMP)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU&6-h)vRCwCNR%=YvWf=dRJLdxDaJYy^ z4bdi`ghz1WGEsDzT65W|<=pzvn$?CiSJ|4HTT4F~8)>z^QCn--ywKc=vY{q{h!<pl zXgbuC<Xkz2bK%^3o)^xM!-1r7^WA>?U*6|^e((ESUr8($!{ZoIJf<O^sbEJ(M*%%v zCafdq3Fh&sE`p12l5n`9qGDhs`XmoC^|^EBR?uUgOeR~U(P+S+cc@e<F&_v75Dtgo z^ZDTS`w@vm&d~4oD=RC{|0iN1RMMkfp-^nk$jE?RuZLVNhg2%Xr2!jSdhTMDUW3{t zb1>JaVM4J1m&=8bkr96Xl-9pX8;H!9xV^pINRQ(>oo;1rZZ3#ifJskWyoDnz7ZB<F z5m2ZhTk;HEU1!Fs#U??_U@O>cHnzgAr02z|sw&q*iHTG#kx0&_r>B=QsZyyFCykKU zp_Bb+ZTSUh_d0>`bup<7{(@SpuBpb`>xz=dtyU|Ow-ZM#kp~|Z)rZKB2~s|(BOt+! zqy6~3=_{xwkcyp0BhU@B;M5O?@m|wSID=BLhGQT*I~!?fX;mcl$v>u3TU%Q>1<q>| z&3j`wijPj&VfDozPt(F|E`-65o>-$)Do|XsfJ_(0pRNG*{pQ4gD<-VO8_&$lq<zNf zNVj>~bRr*qP9_$M7*>A_2iu+Sg`&`A7-2V+VejtcQwC$@-b+}JnFDVyibLmJ*j;0Q zUM>^l6r578*V{m1wJ9fVZEfWpRTvD0MD#XZb_>#pBjUadU)Ps#-Z(vNsxbN2S3Dx^ zSQlpnMg1z`cp`<EeDP*HN13$862f)cgLnw|z2X^mTt(Dxh2_eT6vBYdn|QxQYp**Z zLKlO5P8?fPh{+FYXd)rlaW9ah)a|ff&(j%@XO}>x%|qsstCQwGUat`{6UXJDP(mN; zjz*(fQiwyL&>Z#$3sE|u-xf?ZpUa6ArL!T^&4s#j8*-PvkTkz$`8<p`ZAt(7tRbO` zb#kDRxfi4a7v-Ls1>$+_^rse_`xnQ|D_83wk&P{lQ96Zv!{ZW5ljQBbNZgu&oQTy^ zrV|UqA(u*$lw@S(Qfld-lqo2}LWqod2qTctyHc*gA2i1@lSf=%Jnd6XOwkvn9Vz01 z`ZQ@`$2>IY1{^<oAB*)zF>BAA2QD~$0KV2QaM-;Sx<X?TX_iLDeiiz-mQ&z{Q%+1# z=jAfT0*u0JRg#iWU=(xlSK_;~vs1`vYj;9j{tEJAcM%-*C#_kMs}jTv)=eCq6k<xH zJKWm1&a%RC(_~^l=1GPU-`(BaXliN_-AK1_KWfSrBN#{~E}N|o`k0toCUv-LDW*0W zjm=clOXKbBDLU~&qY^hAW6|CG&PqrnrxHq?PNzU)V<XnCU5l!hc4GPQ9^4xD&LUc0 zpo_xm9n;6eR6f6(syh{5ph14nj2Cx!BwFl0V;9xy+b#LnwQHBy{ZzwjHsje%&*6;| z7HAYwd{}FQBH-dQh23r!;ZRmqwuqjClcV}-K|#SuHXREy46*>WR~nHf9}7l{#UlFb zR0sxx6x~uGyQA6&P1GlJux}j>2j<V8|5KcFs(xJD+uPgbcDoIEd3mD1YqeT9BV@KL zb<znpZ{9o>xmj7LtE&T@2FH4a$*(HtVqS3-Vq)Gb)qi_&ad9<?U!SfYUnPMCN_OAS z(2zKV$K!!6>cQyfC@$Xh;<Gku!p9{gC8)2j7o@kH0c>2Tf;#L@kn%-<Dwemfu<*b{ z(&?@~G9vt@tE+1h1%);zCr6x|n+N+{tyT*wG&eV6!-frs3m;i2e%TYGY<{W@-et>{ zeNW`uxUtRTvX@T(8~XbCo+ndfa$Dne$>dyn^Yil`ltl_0zTB}M4!q&vVX+Bvm7}Pr zsEOp?A~D~;x!%eMACO63@9*!=A(N~5qzvw{T=d7Ai%1u0^a6B2^b?_#Qs*E|H%vb1 zjPKPnN(np3Z`Ff?gXv^KEs;sdtbF}t(+6o2QA$Ct$z&R#LexgT*{9wA>q0+am<a0$ xH3S18ew*OJ&F$$d;pd4d^Qed)>G)fK0RU%d9^ui35y$`l002ovPDHLkV1ls88%zKI literal 0 HcmV?d00001 diff --git a/public/images/redmine_create.png b/public/images/redmine_create.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f8c63a689b2a48c618bfa0c6d813ac6efa5c93 GIT binary patch literal 1510 zcmV<C1sVE@P)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%h)G02RCwCNR(nj<Wf*?X;he*{0OumN zadCi6q%BdX0Fz*;IJ5p}Yt#8x&8^kCtk!g`TC=iI1{=z>tW|5QRBC2!rUh$j5TLW^ znxZn&B!UjN!{OYo^t_*Z$KT<Akh%G7&wiKh^1S!=Q%z4#)7=<qx=TZ|*oCK0pDx5> z3$Ot&1FL4H&I3-M4LDL!QE_cC=cyJlbx%*vT0C|qB_-7ujYeYNosyH2#e5(Tpl~=$ zUayyYJ|9IQkzeq8XJuvOncE@;p$?BmtycR&T3Q;J&1TYQG^AFm1?1rU(P)$jg#b>c zlUyzrzwgELs}LZvXyWefZW|uorKF@hoSB(PAQzCNDwRrr2a`&2CT6e|#>U3j3hh|( zWK~s_b3tN|8gOVwYHDgZlj?Ljp(&e}p(K?W&(>O!kB*Kqc{k>3K^|NvxSHX|7%6M2 zG;#1+KiT)aPl5g`q|zmm{+?pW+PRC8tV?1-Nx{<7(`jU6qzb-oz~@Wzrc+l}S2?uW zt~9UI*7sWv{nPj^>GGCRXw(i3BNXv_DG~~h)|O88#&^hfrc<otJq#AyYPGUOp2yj% z=G7P)?T3kD8~G5{(%sWRgS&RnG|Z_^vyg4uc5<9JD)!j6K25{Td&u$A339Zbq}(^_ z$yiw}_Ot1k9Kosfcsxy5UYnqCXJ;o%RAI4L;!Ml%U%yJBU_iii;T&1&9;2cIEdnWJ z733SdF6P3KFa?JPV*x0&=d6GbHQ-p9K&;p6pOrZ(38!(`UwiAxT(g0S8V(9gmNkAt zD!3pPop>**Z>F^MkJ7TH&&X6-PS@XiUF=l~&v_4yZ3)Ei!y}l8OPSH{TWR2nkLc{? zwKURxf|l<&5J!5`VHzrW>jN5W`IOFW*+j!f57F51Z^Rk$JU$zdzcqn46bj|CKa_d; z_t&3OI25E%02RsaTeu7+>&1G89mL|_3^@lX%w3c~jNCH_^UmZozu$N85am7f9I1>c zWUH-PkT`waCembNQNb&3(7pRx;&PqO1F>PwsFuZmG1%Hd?hEJ1R#i<8eA`C)+`I*e ztq;}E{YOsG_?cg+zu|34Ew3bN-Q%PvQmO=EL|+&ZX?c<G@6V^me)1Uo`Q^u?&(5KB zXRqEgsiJa8frXW{I_lW?AO*+A#CN81lFaKji+N5h1a3IN)QCC{mpNX*W9mw;cxPyq zWF%;(>&OyFh%<{Vw$IgfNt99spA8Oo0x?o)fLj~aSsv~``Y4Lc3>AWIc1>6LqXM(t zq}Cf^`@G11K97mHWun7fNiem|W;=zVzLF*712$fD#<f4?yCC*N;!H2izxIc&n3mig zWxr-(l+VL+b*GZE81RFUi#z8Xr<bfg{B8Woa?9ZIY;zDwW2{RtDbke2$H&EVSy|a~ zJcnjCiA#lrg>Br(d9j2%L`dn4+!8$Gdbyv;CnhGSprGKSB%P}tmt4Gfv1@8-%95R( zEi{&`k!!eeq%@gs#KOy-I6t#?O2N?<dGB~hNy!>4>6`DCc?k=e;7_mJZl_={C?sb8 za8qZ=CnqQ4f{V%cKi@cbk2EiX_dsY*adGj7O42#6w;<iUdiCn#czTA1hXtt=!0-2q zrx?NM8<@H9F?bz=*Rc?MMrmnjE1nIDz3ippe^YO7?-MYUmD?JdT3&zh^Ydd1IeG^M z24dGAwg_BBaMi?$6)TP*+F!&n?|-@8CIPQG9FAu$U%s5-cDoHGlZhA{<=UAp#U|xo z;%H{zg6IR`C{kx1rn&h?ZtGr+u@cyU;<9FFXebrFG=WSF7x49$3<g6GLPU{*o}8Q< z7Yb1qezQ-1|6dn+#mE6R0@VPAxV%ja032~0z>mt5xl_b<bo?v808WKDhmcU*Z2$lO M07*qoM6N<$f>;OR-~a#s literal 0 HcmV?d00001 diff --git a/public/images/redmine_goto.png b/public/images/redmine_goto.png new file mode 100644 index 0000000000000000000000000000000000000000..39bd0ae2c5c0d6d4b6b36049187ea56dd4d1ba01 GIT binary patch literal 1555 zcmV+u2JHEXP)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%wMj%lRCwCNR(nj9Wf*_X;he)cT+caN zWau=oi8M2W3NQ(mh?u#^wdwq;=4v&ntu<Yv*6cRRI2-D;tQBjtOlwAM1O;nr5a6ZB zP`S-Ch_J)`z`1a4(C>N2`}x6vLFMMV{q}w5e9!y-p8NA^rl+UrPAo0mp`}}@;JI_> zQt;RfYz1P0jG0fZfCH!lPG@Il-&o3dn#Ek*($caCk9Wet!tx^`B8Y)^3J(t#-@RTh z1p)zbyWQmRcqkYQ{(`>`<mBX>|4+mqG~i*=>-8_h#Kcf+Y%J+?I?`&j0&?;Gsi`R@ z6bd*T4w{&l;P)f={0bBZE}6Kwx!H=xw^3114<{xj637K4sYas_;KihpoQWCig`uG# z_Ch^Gp3Ke7bu3B@QWFksh>niVVp4;_AS`9~GSsBf;<;8!^1;DDCU3^~%^>$JmR!dW zM}?FvRlC@C;~LqIzDM3`U8FIDlkx5}T6W+dg<0YhrKDo<@$ob;Fp!Jbm*MA23zkz; zQ&SeK+NXA}wAS-m3;k_-n+z*fl7G++3xgD#bW_mpCA~GC?6$YabG}i?@*V~&Zn0R{ zBPBRHV?m2y(Q&v~*~o{mmClw1>N~ifrr}O)jG3%^_t9weS+U2u=V`im>@bb~R86Dx zHMIQoLllvdC-$@JnH<Tfb-7&S5Z@ePabsg6Ym{v^n`gO}G1+sS{64S1#Fa~AE_{qq zPgDv-WoDD7uSa|f1Ow!|*{39+w4SR1O5}rMWeBm+XnaoAs3e@mVSgSuM6vl>DYfjR zuw=FEW6~f5N_OHs_0TbjDR`7tmw!r;_hnJfyQN~UT6wN}aO@2sMjRf&hgl^v_}v-m zt@w~G?$|^F_0_cI@QGQZw*sc4)HmLzzRFMN((dha^Xw@a`u=NihCGkYM&|DcA@=+I z%Q+tEI{oX*&nV#ck>87sH0fEq4Tc+qJi`uR@!t$NCo0^X8bXZPGYR+3)HT1~d-4>m zc<Oo5L`0EwbK#=I@msc&E@2tn_3~?U&+*Dxwa({(*feie%VxmnJ5x!{E3IVB&7%jt zsUzd^6^jyE9?GZtPuJ4$`CsW;*_#xdl|z=oouq0~nh;`SUjQ2ESxCh9=UTGYRM8)w ze?-QlWZH7^`Yo5v>0FBY^ag6!_8|F&hs5th#~8&H><}_sT1ebLh^vuxE^c!yz-zp# zUA!}NaS0*TUAS;Td{ZkS3@}@jeG}asqLniEY;ZV3h*3(tJleR=@^b#(O;gy+&>-pN z_Vl*4Hma(s64OYMi&z>>%04&hpU-1r9+{YMT_LWvTCKHc>g!okK49B*$E@+Ee&;nh zIx0|6Q9%U-1+;G6Izh1MhbF~J9*=TfGcnrdSMyD$^ov-CLj*T>t~)L-Ih9N%6P1>h ziUa3c85tS0Yu7IF`FzThVp5bT4G#~qh$}NQa}A#TbF;c5B_*Zq^5x5q^U#+gP@O=S zGz(@l6UroFQYK_D`N+r!fwV@F&Nq+KuU@^{G(J9VPD)A=7R!0eQ%1dti;KlGE-sEr zN=o=(!XOYXm81+-7X5!XJw1IR#Jd-|Wp+TI9PxD9?RH@WYdkqQDTX?0&T*A7=D6By zHbLriI+d78Qr?4(<w{FS`#?>)(A6glgkQ9_w(f+>A`=o4lvvBY%igH1t)-%(BC%m? zY)n`y%ZN$&>H|-?Hf-2%2IS8`EVz`*UNrtMZ*OmZ0<N;~SmSXi<GyOuDn*bJhb@)Y zA65dPV&z8Gu3cLN`4=JP{x{d#FyK|V^x4kN&IGv96d4&w434>eW=gSjoJ?#n0~bUO z2nUf9NAbCQuBA)9S7WUQ_M^FM?C<Z7h6^J>rbQC*^_R=c2PLLZg0AG`<OwvQCj8AY zZTnvrdeuq>wgGv78IZS$UJf(R0Q{&fncGEtTh~7V3;;^aEcp|zIM4t9002ovPDHLk FV1kL00Nwxq literal 0 HcmV?d00001 diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 628d807d4c..41ac34038b 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -579,6 +579,22 @@ table.errs tr.resolved td > * { background: transparent url(images/icons/thumbs-up.png) 6px 5px no-repeat; } +#action-bar a.lighthouseapp_create { + background: transparent url(/images/lighthouseapp_create.png) 6px 5px no-repeat; +} + +#action-bar a.redmine_create { + background: transparent url(/images/redmine_create.png) 6px 5px no-repeat; +} + +#action-bar a.lighthouseapp_goto { + background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat; +} + +#action-bar a.redmine_goto { + background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat; +} + /* Notices Pagination */ .notice-pagination { float: left; From f79e308d0b3fc6237ae06f2c05b6c3d5e6f703eb Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 1 Apr 2011 18:28:26 +0400 Subject: [PATCH 083/115] Default url options for production env. Thanks khoan for the fix. --- config/initializers/default_url_options.rb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 config/initializers/default_url_options.rb diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb new file mode 100644 index 0000000000..0355b0d941 --- /dev/null +++ b/config/initializers/default_url_options.rb @@ -0,0 +1,3 @@ +(Errbit::Application.config.action_mailer.default_url_options ||= {}).tap do |default| + default.merge! :host => Errbit::Config.host if default[:host].blank? +end From b89db9f1f027a1809f3c5d2a1dbfc1678b65d662 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 1 Apr 2011 18:36:43 +0400 Subject: [PATCH 084/115] Moved default url options setting to _load_config initializer. --- config/initializers/_load_config.rb | 4 +++- config/initializers/default_url_options.rb | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 config/initializers/default_url_options.rb diff --git a/config/initializers/_load_config.rb b/config/initializers/_load_config.rb index e52b66113b..3806c23220 100644 --- a/config/initializers/_load_config.rb +++ b/config/initializers/_load_config.rb @@ -15,4 +15,6 @@ end # Set config specific values -ActionMailer::Base.default_url_options[:host] = Errbit::Config.host \ No newline at end of file +(Errbit::Application.config.action_mailer.default_url_options ||= {}).tap do |default| + default.merge! :host => Errbit::Config.host if default[:host].blank? +end \ No newline at end of file diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb deleted file mode 100644 index 0355b0d941..0000000000 --- a/config/initializers/default_url_options.rb +++ /dev/null @@ -1,3 +0,0 @@ -(Errbit::Application.config.action_mailer.default_url_options ||= {}).tap do |default| - default.merge! :host => Errbit::Config.host if default[:host].blank? -end From cda23ec8ae7e71a60f01ce8e398dcc9092e499cc Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Sat, 2 Apr 2011 13:28:20 +0400 Subject: [PATCH 085/115] Failing specs for redmine issues tracker. --- spec/controllers/errs_controller_spec.rb | 32 +++++++++++++++++++++++ spec/factories/issue_tracker_factories.rb | 5 ++++ 2 files changed, 37 insertions(+) diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 4163bd306c..333e3e7454 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -253,6 +253,38 @@ err.issue_link.should == @issue_link.sub(/\.xml$/, '') end end + + context "redmine tracker" do + let(:notice) { Factory :notice } + let(:tracker) { Factory :redmine_tracker, :app => notice.err.app } + let(:err) { notice.err } + + before(:each) do + number = 5 + @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/issues/#{number}.xml" + body = "<issue></issue>" + stub_request(:post, "#{tracker.account}/projects/#{tracker.project_id}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) + + post :create_issue, :app_id => err.app.id, :id => err.id + err.reload + end + + it "should make request to Lighthouseapp with err params" do + requested = have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") + WebMock.should requested.with(:headers => {'X-Lighthousetoken' => tracker.api_token}) + WebMock.should requested.with(:body => /<tag>errbit<\/tag>/) + WebMock.should requested.with(:body => /<title>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/title>/) + WebMock.should requested.with(:body => /<body>.+<\/body>/m) + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + + it "should create issue link for err" do + err.issue_link.should == @issue_link.sub(/\.xml$/, '') + end + end end context "absent issue tracker" do diff --git a/spec/factories/issue_tracker_factories.rb b/spec/factories/issue_tracker_factories.rb index b31eaae760..a1bb16005a 100644 --- a/spec/factories/issue_tracker_factories.rb +++ b/spec/factories/issue_tracker_factories.rb @@ -4,4 +4,9 @@ e.api_token { Factory.next :word } e.project_id { Factory.next :word } e.association :app, :factory => :app +end + +Factory.define :redmine_tracker, :parent => :lighthouseapp_tracker do |e| + e.issue_tracker_type 'redmine' + e.account { "http://#{Factory.next(:word)}.com" } end \ No newline at end of file From 5b0a2d55ca4a19cc64dc053cce02bc24e63d8514 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Mon, 4 Apr 2011 16:39:30 +0400 Subject: [PATCH 086/115] Using ERB to render lighthouseapp body. --- app/models/issue_tracker.rb | 68 ++++------------------- app/views/errs/lighthouseapp_body.txt.erb | 34 ++++++++++++ 2 files changed, 45 insertions(+), 57 deletions(-) create mode 100644 app/views/errs/lighthouseapp_body.txt.erb diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index a770c570f8..bac6f1a6ca 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -15,6 +15,16 @@ class IssueTracker field :issue_tracker_type, :type => String, :default => 'lighthouseapp' def create_issue err + return create_lighthouseapp_issue err if issue_tracker_type == 'lighthouseapp' + create_redmine_issue err if issue_tracker_type == 'redmine' + end + + protected + def create_redmine_issue err + + end + + def create_lighthouseapp_issue err Lighthouse.account = account Lighthouse.token = api_token @@ -24,69 +34,13 @@ def create_issue err ticket = Lighthouse::Ticket.new(:project_id => project_id) ticket.title = "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" - ticket.body = "" - ticket.body += "[See this exception on Errbit](#{ app_err_url err.app, err } \"See this exception on Errbit\")" - ticket.body += "\n" - if notice = err.notices.first - ticket.body += "# #{notice.message} #" - ticket.body += "\n" - ticket.body += "## Summary ##" - ticket.body += "\n" - if notice.request['url'].present? - ticket.body += "### URL ###" - ticket.body += "\n" - ticket.body += "[#{notice.request['url']}](#{notice.request['url']})" - ticket.body += "\n" - end - ticket.body += "### Where ###" - ticket.body += "\n" - ticket.body += notice.err.where - ticket.body += "\n" - - ticket.body += "### Occured ###" - ticket.body += "\n" - ticket.body += notice.created_at.to_s(:micro) - ticket.body += "\n" - - ticket.body += "### Similar ###" - ticket.body += "\n" - ticket.body += (notice.err.notices.count - 1).to_s - ticket.body += "\n" - - ticket.body += "## Params ##" - ticket.body += "\n" - ticket.body += "<code>#{pretty_hash(notice.params)}</code>" - ticket.body += "\n" - - ticket.body += "## Session ##" - ticket.body += "\n" - ticket.body += "<code>#{pretty_hash(notice.session)}</code>" - ticket.body += "\n" - - ticket.body += "## Backtrace ##" - ticket.body += "\n" - ticket.body += "<code>" - for line in notice.backtrace - ticket.body += "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> **#{line['method']}**" - ticket.body += "\n" - end - ticket.body += "</code>" - ticket.body += "\n" - - ticket.body += "## Environment ##" - ticket.body += "\n" - for key, val in notice.env_vars - ticket.body += "#{key}: #{val}" - end - ticket.body += "\n" - end + ticket.body = ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, '')).result(binding) ticket.tags << "errbit" ticket.save! err.update_attribute :issue_link, "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, '') end - protected def check_params blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } if blank_flags.any? && !blank_flags.all? diff --git a/app/views/errs/lighthouseapp_body.txt.erb b/app/views/errs/lighthouseapp_body.txt.erb new file mode 100644 index 0000000000..c0c6d81111 --- /dev/null +++ b/app/views/errs/lighthouseapp_body.txt.erb @@ -0,0 +1,34 @@ +[See this exception on Errbit](<%= app_err_url err.app, err %> "See this exception on Errbit") +<% if notice = err.notices.first %> + # <%= notice.message %> # + ## Summary ## + <% if notice.request['url'].present? %> + ### URL ### + [<%= notice.request['url'] %>](<%= notice.request['url'] %>)" + <% end %> + ### Where ### + <%= notice.err.where %> + + ### Occured ### + <%= notice.created_at.to_s(:micro) %> + + ### Similar ### + <%= (notice.err.notices.count - 1).to_s %> + + ## Params ## + <code><%= pretty_hash(notice.params) %></code> + + ## Session ## + <code><%= pretty_hash(notice.session) %></code> + + ## Backtrace ## + <code> + <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** + <% end %> + </code> + + ## Environment ## + <% for key, val in notice.env_vars %> + <%= key %>: <%= val %> + <% end %> +<% end %> From 5c3489a2e9c7930a693f15bda0108bc63396d38a Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Tue, 5 Apr 2011 14:02:22 +0400 Subject: [PATCH 087/115] Redmine client added. --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Gemfile b/Gemfile index f005b2ac8b..f0f9cc63a3 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'haml' gem 'will_paginate' gem 'devise', '~> 1.1.8' gem 'lighthouse-api' +gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" platform :ruby do gem 'bson_ext', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 4247bf2586..d711188109 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: git://github.com/oruen/redmine_client.git + revision: ab198901fdd7f67f43614c8e72e6c10f38c9387a + specs: + redmine_client (0.0.1) + activeresource (>= 2.3.0) + GEM remote: http://rubygems.org/ specs: @@ -122,6 +129,7 @@ DEPENDENCIES mongoid (~> 2.0.0.rc.7) nokogiri rails (= 3.0.5) + redmine_client! rspec (~> 2.5) rspec-rails (~> 2.5) webmock From e012272c11cd3581404aa6bb42313565aafc076c Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Tue, 5 Apr 2011 16:01:48 +0400 Subject: [PATCH 088/115] Redmine issue tracker integration. --- Gemfile.lock | 2 +- app/models/issue_tracker.rb | 17 ++++++++++-- app/views/errs/redmine_body.txt.erb | 34 ++++++++++++++++++++++++ spec/controllers/errs_controller_spec.rb | 13 +++++---- 4 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 app/views/errs/redmine_body.txt.erb diff --git a/Gemfile.lock b/Gemfile.lock index d711188109..69b33db007 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: git://github.com/oruen/redmine_client.git - revision: ab198901fdd7f67f43614c8e72e6c10f38c9387a + revision: 0df20a8b695869b03cfa129560b938a0af346add specs: redmine_client (0.0.1) activeresource (>= 2.3.0) diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index bac6f1a6ca..f7496ff4cd 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -21,7 +21,16 @@ def create_issue err protected def create_redmine_issue err - + token = api_token + RedmineClient::Base.configure do + self.token = token + end + RedmineClient::Issue.site = account + "/projects/:project_id" + issue = RedmineClient::Issue.new(:project_id => project_id) + issue.subject = issue_title err + issue.description = ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb").gsub(/^\s*/, '')).result(binding) + issue.save! + err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml$/, '') end def create_lighthouseapp_issue err @@ -32,7 +41,7 @@ def create_lighthouseapp_issue err Lighthouse::Ticket.site ticket = Lighthouse::Ticket.new(:project_id => project_id) - ticket.title = "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" + ticket.title = issue_title err ticket.body = ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, '')).result(binding) @@ -41,6 +50,10 @@ def create_lighthouseapp_issue err err.update_attribute :issue_link, "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, '') end + def issue_title err + "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" + end + def check_params blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } if blank_flags.any? && !blank_flags.all? diff --git a/app/views/errs/redmine_body.txt.erb b/app/views/errs/redmine_body.txt.erb new file mode 100644 index 0000000000..4fb2bec717 --- /dev/null +++ b/app/views/errs/redmine_body.txt.erb @@ -0,0 +1,34 @@ +"See this exception on Errbit":<%= app_err_url err.app, err %> +<% if notice = err.notices.first %> + h1. <%= notice.message %> + h2. Summary + <% if notice.request['url'].present? %> + h3. URL + [<%= notice.request['url'] %>](<%= notice.request['url'] %>)" + <% end %> + h3. Where + <%= notice.err.where %> + + h3. Occured + <%= notice.created_at.to_s(:micro) %> + + h3. Similar + <%= (notice.err.notices.count - 1).to_s %> + + h2. Params + <pre><%= pretty_hash(notice.params) %></pre> + + h2. Session + <pre><%= pretty_hash(notice.session) %></pre> + + h2. Backtrace + <pre> + <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> *<%= line['method'] %>* + <% end %> + </pre> + + h2. Environment + <% for key, val in notice.env_vars %> + <%= key %>: <%= val %> + <% end %> +<% end %> diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 333e3e7454..145ba7af83 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -262,19 +262,18 @@ before(:each) do number = 5 @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/issues/#{number}.xml" - body = "<issue></issue>" + body = "<issue><id type=\"integer\">#{number}</id></issue>" stub_request(:post, "#{tracker.account}/projects/#{tracker.project_id}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) post :create_issue, :app_id => err.app.id, :id => err.id err.reload end - it "should make request to Lighthouseapp with err params" do - requested = have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") - WebMock.should requested.with(:headers => {'X-Lighthousetoken' => tracker.api_token}) - WebMock.should requested.with(:body => /<tag>errbit<\/tag>/) - WebMock.should requested.with(:body => /<title>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/title>/) - WebMock.should requested.with(:body => /<body>.+<\/body>/m) + it "should make request to Redmine with err params" do + requested = have_requested(:post, "#{tracker.account}/projects/#{tracker.project_id}/issues.xml") + WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) + WebMock.should requested.with(:body => /<subject>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/subject>/) + WebMock.should requested.with(:body => /<description>.+<\/description>/m) end it "should redirect to err page" do From d55b077661c7d225711d772cee1717a81728cc09 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Tue, 5 Apr 2011 16:31:16 +0400 Subject: [PATCH 089/115] Memoizing issue templates. --- app/models/issue_tracker.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index f7496ff4cd..44d3bfe9f8 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -28,7 +28,7 @@ def create_redmine_issue err RedmineClient::Issue.site = account + "/projects/:project_id" issue = RedmineClient::Issue.new(:project_id => project_id) issue.subject = issue_title err - issue.description = ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb").gsub(/^\s*/, '')).result(binding) + issue.description = self.class.redmine_body_template.result(binding) issue.save! err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml$/, '') end @@ -43,7 +43,7 @@ def create_lighthouseapp_issue err ticket = Lighthouse::Ticket.new(:project_id => project_id) ticket.title = issue_title err - ticket.body = ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, '')).result(binding) + ticket.body = self.class.lighthouseapp_body_template.result(binding) ticket.tags << "errbit" ticket.save! @@ -65,4 +65,14 @@ def check_params errors.add(:base, message) end end + + class << self + def lighthouseapp_body_template + @@lighthouseapp_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, '')) + end + + def redmine_body_template + @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb").gsub(/^\s*/, '')) + end + end end From d95174a2fe28442db21a7391d152a81886e74a9a Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Thu, 7 Apr 2011 18:24:38 +0400 Subject: [PATCH 090/115] Fixing new notify options for legacy apps. --- app/models/app.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/models/app.rb b/app/models/app.rb index a35354842a..679ae55703 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -47,6 +47,17 @@ def self.find_by_api_key!(key) def last_deploy_at deploys.last && deploys.last.created_at end + + # Legacy apps don't have notify_on_errs and notify_on_deploys params + def notify_on_errs + !(self[:notify_on_errs] == false) + end + alias :notify_on_errs? :notify_on_errs + + def notify_on_deploys + !(self[:notify_on_deploys] == false) + end + alias :notify_on_deploys? :notify_on_deploys protected From 3484c2429e2cf32fb9193fb3051a0e76905f49c9 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 8 Apr 2011 14:51:09 +0400 Subject: [PATCH 091/115] Using /issues.xml for Redmine issue creation. Overriding RedmineClient::Base.site on every request. --- app/controllers/errs_controller.rb | 3 ++- app/models/issue_tracker.rb | 5 +++-- spec/controllers/errs_controller_spec.rb | 11 ++++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index d09074e18f..a5cae56375 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -36,7 +36,8 @@ def create_issue flash[:error] = "This up has no issue tracker setup." end redirect_to app_err_path(@app, @err) - rescue ActiveResource::ConnectionError + rescue ActiveResource::ConnectionError => e + Rails.logger.error e.to_s flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later." redirect_to app_err_path(@app, @err) end diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index 44d3bfe9f8..72c50ac0b1 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -22,15 +22,16 @@ def create_issue err protected def create_redmine_issue err token = api_token + acc = account RedmineClient::Base.configure do self.token = token + self.site = acc end - RedmineClient::Issue.site = account + "/projects/:project_id" issue = RedmineClient::Issue.new(:project_id => project_id) issue.subject = issue_title err issue.description = self.class.redmine_body_template.result(binding) issue.save! - err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml$/, '') + err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}") end def create_lighthouseapp_issue err diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 145ba7af83..86f8fb4051 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -261,17 +261,18 @@ before(:each) do number = 5 - @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/issues/#{number}.xml" - body = "<issue><id type=\"integer\">#{number}</id></issue>" - stub_request(:post, "#{tracker.account}/projects/#{tracker.project_id}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) + @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" + body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" + stub_request(:post, "#{tracker.account}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) post :create_issue, :app_id => err.app.id, :id => err.id err.reload end it "should make request to Redmine with err params" do - requested = have_requested(:post, "#{tracker.account}/projects/#{tracker.project_id}/issues.xml") + requested = have_requested(:post, "#{tracker.account}/issues.xml") WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) + WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) WebMock.should requested.with(:body => /<subject>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/subject>/) WebMock.should requested.with(:body => /<description>.+<\/description>/m) end @@ -281,7 +282,7 @@ end it "should create issue link for err" do - err.issue_link.should == @issue_link.sub(/\.xml$/, '') + err.issue_link.should == @issue_link.sub(/\.xml/, '') end end end From e6b0a1780a25f5c755704c9c0d4198bea53df5b6 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 8 Apr 2011 15:34:24 +0400 Subject: [PATCH 092/115] Redmine formating fix. --- app/models/issue_tracker.rb | 2 +- app/views/errs/redmine_body.txt.erb | 73 +++++++++++++++++------------ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index 72c50ac0b1..717cd37594 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -73,7 +73,7 @@ def lighthouseapp_body_template end def redmine_body_template - @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb").gsub(/^\s*/, '')) + @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb")) end end end diff --git a/app/views/errs/redmine_body.txt.erb b/app/views/errs/redmine_body.txt.erb index 4fb2bec717..aeff23cfe2 100644 --- a/app/views/errs/redmine_body.txt.erb +++ b/app/views/errs/redmine_body.txt.erb @@ -1,34 +1,45 @@ "See this exception on Errbit":<%= app_err_url err.app, err %> <% if notice = err.notices.first %> - h1. <%= notice.message %> - h2. Summary - <% if notice.request['url'].present? %> - h3. URL - [<%= notice.request['url'] %>](<%= notice.request['url'] %>)" - <% end %> - h3. Where - <%= notice.err.where %> - - h3. Occured - <%= notice.created_at.to_s(:micro) %> - - h3. Similar - <%= (notice.err.notices.count - 1).to_s %> - - h2. Params - <pre><%= pretty_hash(notice.params) %></pre> - - h2. Session - <pre><%= pretty_hash(notice.session) %></pre> - - h2. Backtrace - <pre> - <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> *<%= line['method'] %>* - <% end %> - </pre> - - h2. Environment - <% for key, val in notice.env_vars %> - <%= key %>: <%= val %> - <% end %> +h1. <%= notice.message %> + +h2. Summary +<% if notice.request['url'].present? %> +h3. URL + +"<%= notice.request['url'] %>":<%= notice.request['url'] %> +<% end %> +h3. Where + +<%= notice.err.where %> + +h3. Occured + +<%= notice.created_at.to_s(:micro) %> + +h3. Similar + +<%= (notice.err.notices.count - 1).to_s %> + +h2. Params + +<pre><%= pretty_hash(notice.params) %></pre> + +h2. Session + +<pre><%= pretty_hash(notice.session) %></pre> + +h2. Backtrace + +<pre> +<% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> *<%= line['method'] %>* +<% end %> +</pre> + +h2. Environment + +<pre> +<% for key, val in notice.env_vars %> +<%= key %>: <%= val %> +<% end %> +</pre> <% end %> From dd383c8fb457da12320a5d054ef08b8d27fe0967 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Fri, 8 Apr 2011 15:40:05 +0400 Subject: [PATCH 093/115] Note on Redmine integration. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bf384c9b9..e20a020d0a 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,14 @@ Lighthouseapp integration * Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview * Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it. -* Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview +* Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview + +Redmine integration +------------------------- + +* Account is the host of your redmine installation, i.e. **http://redmine.org** +* Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it. +* Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject TODO ---- From fd02658885f543aab96444c73bc6abf15e366bdf Mon Sep 17 00:00:00 2001 From: Ben Langfeld <ben@langfeld.me> Date: Wed, 13 Apr 2011 07:27:17 -0700 Subject: [PATCH 094/115] Use bundler's built in capistrano integration --- config/deploy.example.rb | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/config/deploy.example.rb b/config/deploy.example.rb index ae5e2b7289..ee4503c511 100644 --- a/config/deploy.example.rb +++ b/config/deploy.example.rb @@ -6,6 +6,8 @@ # `cap deploy` whenever you would like to deploy Errbit. Refer # to the Readme for more information. +require 'bundler/capistrano' + set :application, "errbit" set :repository, "http://github.com/jdpace/errbit.git" @@ -39,19 +41,6 @@ end end -namespace :bundler do - task :symlink_vendor, :roles => :app, :except => { :no_release => true } do - shared_gems = File.join(shared_path,'vendor','bundler_gems') - release_gems = "#{latest_release}/vendor/" - run("mkdir -p #{shared_gems} && ln -nfs #{shared_gems} #{release_gems}") - end - - task :install, :rolse => :app do - bundler.symlink_vendor - run("cd #{release_path} && bundle install vendor/bundler_gems --without development test") - end -end - namespace :errbit do task :setup_configs do shared_configs = File.join(shared_path,'config') From 6d43ea958b5eba3f1f8dc318e5a2a82c61820133 Mon Sep 17 00:00:00 2001 From: Ben Langfeld <ben@langfeld.me> Date: Wed, 13 Apr 2011 07:29:57 -0700 Subject: [PATCH 095/115] Don't encourage the use of ENV vars --- config/mongoid.example.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/mongoid.example.yml b/config/mongoid.example.yml index 67496d6938..1b14d4df0d 100644 --- a/config/mongoid.example.yml +++ b/config/mongoid.example.yml @@ -26,8 +26,8 @@ test: # set these environment variables on your prod server production: <<: *defaults - host: <%= ENV['MONGOID_HOST'] %> - port: <%= ENV['MONGOID_PORT'] %> - username: <%= ENV['MONGOID_USERNAME'] %> - password: <%= ENV['MONGOID_PASSWORD'] %> - database: <%= ENV['MONGOID_DATABASE'] %> \ No newline at end of file + host: localhost + port: 27018 + username: errbit + password: + database: errbit \ No newline at end of file From 6858ff328c8db3cbacf47200db17bf51e9527474 Mon Sep 17 00:00:00 2001 From: Ben Langfeld <ben@langfeld.me> Date: Wed, 13 Apr 2011 07:57:26 -0700 Subject: [PATCH 096/115] Oops, forgot to remove a callback --- config/deploy.example.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deploy.example.rb b/config/deploy.example.rb index ee4503c511..5a3203e4e1 100644 --- a/config/deploy.example.rb +++ b/config/deploy.example.rb @@ -31,7 +31,7 @@ set(:current_branch) { `git branch`.match(/\* (\S+)\s/m)[1] || raise("Couldn't determine current branch") } set :branch, defer { current_branch } -after 'deploy:update_code', 'errbit:symlink_configs', 'bundler:install' +after 'deploy:update_code', 'errbit:symlink_configs' namespace :deploy do task :start do ; end From 49b0fe02689365104ba52b427922168824e9274d Mon Sep 17 00:00:00 2001 From: Bob Lail <bob.lailfamily@gmail.com> Date: Fri, 22 Apr 2011 12:01:53 -0500 Subject: [PATCH 097/115] use Josh Peek's useragent gem to parse HTTP_USER_AGENT --- Gemfile | 1 + Gemfile.lock | 2 ++ app/models/notice.rb | 5 +++++ spec/models/notice_spec.rb | 13 +++++++++++++ 4 files changed, 21 insertions(+) diff --git a/Gemfile b/Gemfile index f0f9cc63a3..2341de99da 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'will_paginate' gem 'devise', '~> 1.1.8' gem 'lighthouse-api' gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" +gem 'useragent', '~> 0.3.1' platform :ruby do gem 'bson_ext', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 69b33db007..f0a9b55ba4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,6 +109,7 @@ GEM treetop (1.4.9) polyglot (>= 0.3.1) tzinfo (0.3.25) + useragent (0.3.1) warden (1.0.3) rack (>= 1.0.0) webmock (1.6.2) @@ -132,5 +133,6 @@ DEPENDENCIES redmine_client! rspec (~> 2.5) rspec-rails (~> 2.5) + useragent (~> 0.3.1) webmock will_paginate diff --git a/app/models/notice.rb b/app/models/notice.rb index 83b5ce1663..4006722e78 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -46,6 +46,11 @@ def self.from_xml(hoptoad_xml) }) end + def user_agent + agent_string = env_vars['HTTP_USER_AGENT'] + agent_string.blank? ? nil : UserAgent.parse(agent_string) + end + def request read_attribute(:request) || {} end diff --git a/spec/models/notice_spec.rb b/spec/models/notice_spec.rb index 2d89542107..345fc5223c 100644 --- a/spec/models/notice_spec.rb +++ b/spec/models/notice_spec.rb @@ -105,6 +105,19 @@ end end + describe "user agent" do + it "should be parsed and human-readable" do + notice = Factory.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) + notice.user_agent.browser.should == 'Chrome' + notice.user_agent.version.to_s.should =~ /^10\.0/ + end + + it "should be nil if HTTP_USER_AGENT is blank" do + notice = Factory.build(:notice) + notice.user_agent.should == nil + end + end + describe "email notifications" do before do @app = Factory(:app_with_watcher) From 2e279eb197cbe2acc5d3fa8af349d0dad8e283a7 Mon Sep 17 00:00:00 2001 From: Bob Lail <bob.lailfamily@gmail.com> Date: Fri, 22 Apr 2011 12:42:09 -0500 Subject: [PATCH 098/115] added a tally of browsers on which an error occurred to err/summary --- app/helpers/application_helper.rb | 40 +++++++++++++++++++++++++++ app/views/errs/_tally_table.html.haml | 5 ++++ app/views/notices/_summary.html.haml | 5 +++- public/stylesheets/application.css | 23 ++++++++++++++- 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 app/views/errs/_tally_table.html.haml diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a5ba883fe8..2412b76bf0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,5 +1,45 @@ module ApplicationHelper + + def lighthouse_tracker? object object.issue_tracker_type == "lighthouseapp" end + + + def user_agent_graph(error) + tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} + create_percentage_table(tallies, :total => error.notices.count) + end + + def pretty_user_agent(user_agent) + (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" + end + + + def tally(collection, &block) + collection.inject({}) do |tallies, item| + value = yield item + tallies[value] = (tallies[value] || 0) + 1 + tallies + end + end + + + def create_percentage_table(tallies, options={}) + total = (options[:total] || total_from_tallies(tallies)) + percent = 100.0 / total.to_f + rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ + .sort {|a, b| a[0] <=> b[0]} + render :partial => "errs/tally_table", :locals => {:rows => rows} + end + + +private + + + def total_from_tallies(tallies) + tallies.values.inject(0) {|sum, n| sum + n} + end + + end diff --git a/app/views/errs/_tally_table.html.haml b/app/views/errs/_tally_table.html.haml new file mode 100644 index 0000000000..bf54ce587d --- /dev/null +++ b/app/views/errs/_tally_table.html.haml @@ -0,0 +1,5 @@ +%table.tally + - rows.each do |row| + %tr + %td.percent= number_to_percentage(row[0], :precision => 1) + %th.value= row[1] diff --git a/app/views/notices/_summary.html.haml b/app/views/notices/_summary.html.haml index 06889e6912..d38b79a963 100644 --- a/app/views/notices/_summary.html.haml +++ b/app/views/notices/_summary.html.haml @@ -15,4 +15,7 @@ %td= notice.created_at.to_s(:micro) %tr %th Similar - %td= notice.err.notices.count - 1 \ No newline at end of file + %td= notice.err.notices.count - 1 + %tr + %th Browser + %td= user_agent_graph(notice.err) \ No newline at end of file diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 41ac34038b..6886a2e991 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -562,7 +562,7 @@ table.errs td.app .environment { table.errs td.message a { width: 420px; display: block; - word-wrap: break-word; + word-wrap: break-word; } table.errs td.message em { color: #727272; @@ -574,6 +574,27 @@ table.errs tr.resolved td > * { -webkit-opacity: 0.5; } +/* Tally tables */ +table.tally { + border:none; +} +table.tally td, +table.tally th { + border:none !important; + background:none !important; + padding:8px 0 0; +} +table.tally tbody tr:first-child td, +table.tally tbody tr:first-child th { + padding-top:0; +} +table.tally td.percent { + width:4.5em; +} +table.tally th.value { + text-transform:none; +} + /* Resolve Errs */ #action-bar a.resolve { background: transparent url(images/icons/thumbs-up.png) 6px 5px no-repeat; From d0d658c2ea16a7b3468bac97b53ba1ebb84f350a Mon Sep 17 00:00:00 2001 From: Bob Lail <bob.lailfamily@gmail.com> Date: Fri, 22 Apr 2011 12:48:19 -0500 Subject: [PATCH 099/115] the notices API responds to GET requests as well (to support JavaScript notifier) --- app/controllers/notices_controller.rb | 3 ++- spec/controllers/notices_controller_spec.rb | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/controllers/notices_controller.rb b/app/controllers/notices_controller.rb index bdf7374830..69bff71f77 100644 --- a/app/controllers/notices_controller.rb +++ b/app/controllers/notices_controller.rb @@ -4,7 +4,8 @@ class NoticesController < ApplicationController skip_before_filter :authenticate_user!, :only => :create def create - @notice = Notice.from_xml(request.raw_post) + # params[:data] if the notice came from a GET request, raw_post if it came via POST + @notice = Notice.from_xml(params[:data] || request.raw_post) respond_with @notice end diff --git a/spec/controllers/notices_controller_spec.rb b/spec/controllers/notices_controller_spec.rb index 1b9980ef0e..44b286ca0f 100644 --- a/spec/controllers/notices_controller_spec.rb +++ b/spec/controllers/notices_controller_spec.rb @@ -2,7 +2,7 @@ describe NoticesController do - context 'POST[XML] notices#create' do + context 'notices API' do before do @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read @app = Factory(:app_with_watcher) @@ -11,15 +11,21 @@ request.env['Content-type'] = 'text/xml' request.env['Accept'] = 'text/xml, application/xml' - request.should_receive(:raw_post).and_return(@xml) end - it "generates a notice from the xml" do + it "generates a notice from xml [POST]" do Notice.should_receive(:from_xml).with(@xml).and_return(@notice) + request.should_receive(:raw_post).and_return(@xml) post :create end + it "generates a notice from xml [GET]" do + Notice.should_receive(:from_xml).with(@xml).and_return(@notice) + get :create, {:data => @xml} + end + it "sends a notification email" do + request.should_receive(:raw_post).and_return(@xml) post :create email = ActionMailer::Base.deliveries.last email.to.should include(@app.watchers.first.email) From bb5ba2ff8feee03b3b864a1a143782d70b4d6331 Mon Sep 17 00:00:00 2001 From: Bob Lail <bob.lailfamily@gmail.com> Date: Fri, 22 Apr 2011 12:49:37 -0500 Subject: [PATCH 100/115] alphabetized keys in environment --- app/views/notices/_environment.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/notices/_environment.html.haml b/app/views/notices/_environment.html.haml index ebf533ef8b..ff6bf2e1db 100644 --- a/app/views/notices/_environment.html.haml +++ b/app/views/notices/_environment.html.haml @@ -1,6 +1,6 @@ .window %table.environment - - notice.env_vars.each do |key,val| + - notice.env_vars.sort_by {|pair| pair[0]}.each do |pair| %tr - %th= key - %td.main= val \ No newline at end of file + %th= pair[0] + %td.main= pair[1] \ No newline at end of file From 40453cc3539b4c689aea482959a7f126727dfec8 Mon Sep 17 00:00:00 2001 From: Karol Hosiawa <karol.hosiawa@white-space.co.uk> Date: Wed, 6 Apr 2011 15:43:25 +0200 Subject: [PATCH 101/115] moved Notices to a separate collection --- .gitignore | 1 + Gemfile | 3 +- Gemfile.lock | 30 ++++--- README.md | 11 ++- app/controllers/apps_controller.rb | 26 +++--- app/controllers/errs_controller.rb | 26 +++--- app/models/app.rb | 18 ++--- app/models/err.rb | 26 +++--- app/models/notice.rb | 81 ++++++++++++++----- app/views/apps/index.html.haml | 2 +- app/views/apps/show.html.haml | 2 +- app/views/errs/_table.html.haml | 2 +- app/views/errs/lighthouseapp_body.txt.erb | 2 +- app/views/errs/redmine_body.txt.erb | 2 +- app/views/errs/show.html.haml | 10 +-- app/views/mailer/err_notification.text.erb | 6 +- app/views/notices/_atom_entry.html.haml | 4 +- app/views/notices/_environment.html.haml | 2 +- app/views/notices/_summary.html.haml | 2 +- ...027_move_notices_to_separate_collection.rb | 22 +++++ lib/hoptoad.rb | 10 +-- lib/recurse.rb | 24 ++++++ lib/tasks/errbit/err_message.rake | 12 +++ lib/tasks/errbit/notices_counter.rake | 12 +++ spec/controllers/errs_controller_spec.rb | 50 ++++++------ spec/models/err_spec.rb | 71 ++++++++++++---- spec/models/notice_spec.rb | 52 +++++++----- 27 files changed, 342 insertions(+), 167 deletions(-) create mode 100644 db/migrate/20110422152027_move_notices_to_separate_collection.rb create mode 100644 lib/recurse.rb create mode 100644 lib/tasks/errbit/err_message.rake create mode 100644 lib/tasks/errbit/notices_counter.rake diff --git a/.gitignore b/.gitignore index 6137e75503..f976d0d4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ config/deploy.rb config/mongoid.yml .rvmrc *~ +*.rbc diff --git a/Gemfile b/Gemfile index f0f9cc63a3..6b5d1b0cd2 100644 --- a/Gemfile +++ b/Gemfile @@ -2,12 +2,13 @@ source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'nokogiri' -gem 'mongoid', '~> 2.0.0.rc.7' +gem 'mongoid', '2.0.0.rc.8' gem 'haml' gem 'will_paginate' gem 'devise', '~> 1.1.8' gem 'lighthouse-api' gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" +gem 'mongoid_rails_migrations' platform :ruby do gem 'bson_ext', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 69b33db007..4ecb153b72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,15 +35,15 @@ GEM activemodel (= 3.0.5) activesupport (= 3.0.5) activesupport (3.0.5) - addressable (2.2.4) + addressable (2.2.5) arel (2.0.9) bcrypt-ruby (2.1.4) - bson (1.2.4) - bson_ext (1.2.4) + bson (1.3.0) + bson_ext (1.3.0) builder (2.1.2) crack (0.1.8) - database_cleaner (0.6.5) - devise (1.1.8) + database_cleaner (0.6.7) + devise (1.1.9) bcrypt-ruby (~> 2.1.2) warden (~> 1.0.2) diff-lcs (1.1.2) @@ -58,23 +58,28 @@ GEM lighthouse-api (2.0) activeresource (>= 3.0.0) activesupport (>= 3.0.0) - mail (2.2.15) + mail (2.2.17) activesupport (>= 2.3.6) i18n (>= 0.4.0) mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.16) - mongo (1.2.4) - bson (>= 1.2.4) - mongoid (2.0.0.rc.7) + mongo (1.3.0) + bson (>= 1.3.0) + mongoid (2.0.0.rc.8) activemodel (~> 3.0) mongo (~> 1.2) tzinfo (~> 0.3.22) will_paginate (~> 3.0.pre) + mongoid_rails_migrations (0.0.10) + activesupport (~> 3.0.0) + bundler (>= 0.9.19) + rails (~> 3.0.0) + railties (~> 3.0.0) nokogiri (1.4.4) polyglot (0.3.1) rack (1.2.2) - rack-mount (0.6.13) + rack-mount (0.6.14) rack (>= 1.0.0) rack-test (0.5.7) rack (>= 1.0) @@ -108,7 +113,7 @@ GEM thor (0.14.6) treetop (1.4.9) polyglot (>= 0.3.1) - tzinfo (0.3.25) + tzinfo (0.3.26) warden (1.0.3) rack (>= 1.0.0) webmock (1.6.2) @@ -126,7 +131,8 @@ DEPENDENCIES factory_girl_rails haml lighthouse-api - mongoid (~> 2.0.0.rc.7) + mongoid (= 2.0.0.rc.8) + mongoid_rails_migrations nokogiri rails (= 3.0.5) redmine_client! diff --git a/README.md b/README.md index e20a020d0a..e3c8a0e0cc 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,24 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at 4. Enjoy! +Upgrading +--------- +*Note*: If upgrading from a version of Errbit that used Notices embedded in Errs please run: + + 1. git pull origin master ( assuming origin is the github.com/jdpace/errbit repo ) + 2. rake db:migrate + Lighthouseapp integration ------------------------- -* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview +* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview * Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it. * Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview Redmine integration ------------------------- -* Account is the host of your redmine installation, i.e. **http://redmine.org** +* Account is the host of your redmine installation, i.e. **http://redmine.org** * Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it. * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index 3bf60825aa..f3a31cbab0 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -1,12 +1,12 @@ class AppsController < ApplicationController - + before_filter :require_admin!, :except => [:index, :show] before_filter :find_app, :except => [:index, :new, :create] - + def index @apps = current_user.admin? ? App.all : current_user.apps.all end - + def show respond_to do |format| format.html do @@ -18,21 +18,21 @@ def show end end end - + def new @app = App.new @app.watchers.build @app.issue_tracker = IssueTracker.new end - + def edit @app.watchers.build if @app.watchers.none? @app.issue_tracker = IssueTracker.new if @app.issue_tracker.nil? end - + def create @app = App.new(params[:app]) - + if @app.save flash[:success] = 'Great success! Configure your app with the API key below' redirect_to app_path(@app) @@ -40,8 +40,8 @@ def create render :new end end - - def update + + def update if @app.update_attributes(params[:app]) flash[:success] = "Good news everyone! '#{@app.name}' was successfully updated." redirect_to app_path(@app) @@ -49,18 +49,18 @@ def update render :edit end end - + def destroy @app.destroy flash[:success] = "'#{@app.name}' was successfully destroyed." redirect_to apps_path end - + protected - + def find_app @app = App.find(params[:id]) - + # Mongoid Bug: could not chain: current_user.apps.find_by_id! # apparently finding by 'watchers.email' and 'id' is broken raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index a5cae56375..3b18616a69 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -1,8 +1,8 @@ class ErrsController < ApplicationController - + before_filter :find_app, :except => [:index, :all] before_filter :find_err, :except => [:index, :all] - + def index app_scope = current_user.admin? ? App.all : current_user.apps respond_to do |format| @@ -14,14 +14,14 @@ def index end end end - + def all app_scope = current_user.admin? ? App.all : current_user.apps @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) end - + def show - page = (params[:notice] || @err.notices.count) + page = (params[:notice] || @err.notices_count) page = 1 if page.to_i.zero? @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1) @notice = @notices.first @@ -46,25 +46,25 @@ def clear_issue @err.update_attribute :issue_link, nil redirect_to app_err_path(@app, @err) end - + def resolve - # Deal with bug in mogoid where find is returning an Enumberable obj + # Deal with bug in mongoid where find is returning an Enumberable obj @err = @err.first if @err.respond_to?(:first) - + @err.resolve! - + flash[:success] = 'Great news everyone! The err has been resolved.' redirect_to :back rescue ActionController::RedirectBackError redirect_to app_path(@app) end - + protected - + def find_app @app = App.find(params[:app_id]) - + # Mongoid Bug: could not chain: current_user.apps.find_by_id! # apparently finding by 'watchers.email' and 'id' is broken raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) @@ -79,5 +79,5 @@ def set_tracker_params IssueTracker.default_url_options[:port] = request.port IssueTracker.default_url_options[:protocol] = request.scheme end - + end diff --git a/app/models/app.rb b/app/models/app.rb index 3709ae13ff..fb71b370e2 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -1,7 +1,7 @@ class App include Mongoid::Document include Mongoid::Timestamps - + field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false @@ -21,29 +21,29 @@ class App embeds_many :deploys embeds_one :issue_tracker references_many :errs, :dependent => :destroy - + before_validation :generate_api_key, :on => :create - + validates_presence_of :name, :api_key validates_uniqueness_of :name, :allow_blank => true validates_uniqueness_of :api_key, :allow_blank => true validates_associated :watchers validate :check_issue_tracker - + accepts_nested_attributes_for :watchers, :allow_destroy => true, :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) } - + # Mongoid Bug: find(id) on association proxies returns an Enumerator def self.find_by_id!(app_id) where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id)) end - + def self.find_by_api_key!(key) where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key)) end - + def last_deploy_at deploys.last && deploys.last.created_at end @@ -58,9 +58,9 @@ def notify_on_deploys !(self[:notify_on_deploys] == false) end alias :notify_on_deploys? :notify_on_deploys - + protected - + def generate_api_key self.api_key ||= ActiveSupport::SecureRandom.hex end diff --git a/app/models/err.rb b/app/models/err.rb index 290a4829ff..4652588070 100644 --- a/app/models/err.rb +++ b/app/models/err.rb @@ -1,7 +1,7 @@ class Err include Mongoid::Document include Mongoid::Timestamps - + field :klass field :component field :action @@ -10,42 +10,44 @@ class Err field :last_notice_at, :type => DateTime field :resolved, :type => Boolean, :default => false field :issue_link, :type => String + field :notices_count, :type => Integer, :default => 0 + field :message index :last_notice_at index :app_id referenced_in :app - embeds_many :notices - + references_many :notices + validates_presence_of :klass, :environment - + scope :resolved, where(:resolved => true) scope :unresolved, where(:resolved => false) scope :ordered, order_by(:last_notice_at.desc) scope :in_env, lambda {|env| where(:environment => env)} scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} - + def self.for(attrs) app = attrs.delete(:app) app.errs.where(attrs).first || app.errs.create!(attrs) end - + def resolve! self.update_attributes!(:resolved => true) end - + def unresolved? !resolved? end - + def where where = component.dup where << "##{action}" if action.present? where end - + def message - notices.first.try(:message) || klass + super || klass end - -end \ No newline at end of file + +end diff --git a/app/models/notice.rb b/app/models/notice.rb index 83b5ce1663..3d5a8c783a 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -1,32 +1,37 @@ require 'hoptoad' +require 'recurse' class Notice include Mongoid::Document include Mongoid::Timestamps - + field :message field :backtrace, :type => Array field :server_environment, :type => Hash field :request, :type => Hash field :notifier, :type => Hash - - embedded_in :err, :inverse_of => :notices - + + referenced_in :err + index :err_id + after_create :cache_last_notice_at after_create :deliver_notification, :if => :should_notify? - + before_create :increase_counter_cache, :cache_message + before_save :sanitize + before_destroy :decrease_counter_cache + validates_presence_of :backtrace, :server_environment, :notifier - + scope :ordered, order_by(:created_at.asc) - + def self.from_xml(hoptoad_xml) hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml) app = App.find_by_api_key!(hoptoad_notice['api-key']) - + hoptoad_notice['request'] ||= {} hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank? hoptoad_notice['request']['action'] = nil if hoptoad_notice['request']['action'].blank? - + err = Err.for({ :app => app, :klass => hoptoad_notice['error']['class'], @@ -36,7 +41,7 @@ def self.from_xml(hoptoad_xml) :fingerprint => hoptoad_notice['fingerprint'] }) err.update_attributes(:resolved => false) if err.resolved? - + err.notices.create!({ :message => hoptoad_notice['error']['message'], :backtrace => hoptoad_notice['error']['backtrace']['line'], @@ -45,35 +50,67 @@ def self.from_xml(hoptoad_xml) :notifier => hoptoad_notice['notifier'] }) end - + def request read_attribute(:request) || {} end - + def env_vars request['cgi-data'] || {} end - + def params request['params'] || {} end - + def session request['session'] || {} end - + def deliver_notification Mailer.err_notification(self).deliver end - + def cache_last_notice_at err.update_attributes(:last_notice_at => created_at) end - + protected - - def should_notify? - err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? + + def should_notify? + err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? + end + + + def increase_counter_cache + err.inc(:notices_count,1) + end + + def decrease_counter_cache + err.inc(:notices_count,-1) + end + + def cache_message + err.update_attribute(:message, message) if err.notices_count == 1 + end + + def sanitize + [:server_environment, :request, :notifier].each do |h| + send("#{h}=",sanitize_hash(send(h))) end - -end \ No newline at end of file + end + + def sanitize_hash(h) + h.recurse do + |h| h.inject({}) do |h,(k,v)| + if k.is_a?(String) + h[k.gsub(/\./,'.').gsub(/^\$/,'$')] = v + else + h[k] = v + end + h + end + end + end +end + diff --git a/app/views/apps/index.html.haml b/app/views/apps/index.html.haml index c636d814bc..4519184029 100644 --- a/app/views/apps/index.html.haml +++ b/app/views/apps/index.html.haml @@ -14,7 +14,7 @@ %td.name= link_to app.name, app_path(app) %td.deploy= app.last_deploy_at ? link_to( app.last_deploy_at.to_s(:micro), app_deploys_path(app)) : 'n/a' %td.count - - if app.errs.any? + - if app.errs.count > 0 = link_to app.errs.unresolved.count, app_errs_path(app) - else \- diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml index 61b10a4825..200ff75cb1 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -50,7 +50,7 @@ - else %h3 No deploys -- if @app.errs.any? +- if @app.errs.count > 0 %h3.clear Errs = render 'errs/table', :errs => @errs - else diff --git a/app/views/errs/_table.html.haml b/app/views/errs/_table.html.haml index 28efc14f37..825ef7dffb 100644 --- a/app/views/errs/_table.html.haml +++ b/app/views/errs/_table.html.haml @@ -18,7 +18,7 @@ %em= err.where %td.latest #{time_ago_in_words(last_notice_at err)} ago %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a' - %td.count= link_to err.notices.count, app_err_path(err.app, err) + %td.count= link_to err.notices_count, app_err_path(err.app, err) %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? - if errs.none? %tr diff --git a/app/views/errs/lighthouseapp_body.txt.erb b/app/views/errs/lighthouseapp_body.txt.erb index c0c6d81111..709d194f5f 100644 --- a/app/views/errs/lighthouseapp_body.txt.erb +++ b/app/views/errs/lighthouseapp_body.txt.erb @@ -13,7 +13,7 @@ <%= notice.created_at.to_s(:micro) %> ### Similar ### - <%= (notice.err.notices.count - 1).to_s %> + <%= (notice.err.notices_count - 1).to_s %> ## Params ## <code><%= pretty_hash(notice.params) %></code> diff --git a/app/views/errs/redmine_body.txt.erb b/app/views/errs/redmine_body.txt.erb index aeff23cfe2..2a7e975ec4 100644 --- a/app/views/errs/redmine_body.txt.erb +++ b/app/views/errs/redmine_body.txt.erb @@ -18,7 +18,7 @@ h3. Occured h3. Similar -<%= (notice.err.notices.count - 1).to_s %> +<%= (notice.err.notices_count - 1).to_s %> h2. Params diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index ade5c90cd1..f7fb535b15 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -20,7 +20,7 @@ %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' %h4= @notice.try(:message) - + = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination' viewing occurrence #{@notices.current_page} of #{@notices.total_pages} @@ -36,19 +36,19 @@ viewing occurrence #{@notices.current_page} of #{@notices.total_pages} #summary %h3 Summary = render 'notices/summary', :notice => @notice - + #backtrace %h3 Backtrace = render 'notices/backtrace', :lines => @notice.backtrace - + #environment %h3 Environment = render 'notices/environment', :notice => @notice - + #params %h3 Parameters = render 'notices/params', :notice => @notice - + #session %h3 Session = render 'notices/session', :notice => @notice diff --git a/app/views/mailer/err_notification.text.erb b/app/views/mailer/err_notification.text.erb index 3d46554f67..dfd8a31597 100644 --- a/app/views/mailer/err_notification.text.erb +++ b/app/views/mailer/err_notification.text.erb @@ -1,7 +1,7 @@ An err has just occurred in <%= @notice.err.environment %>: <%= @notice.err.message %> -This err has occurred <%= pluralize @notice.err.notices.count, 'time' %>. You should really look into it here: +This err has occurred <%= pluralize @notice.err.notices_count, 'time' %>. You should really look into it here: <%= app_err_url(@app, @notice.err) %> - -<%= render :partial => 'signature' %> \ No newline at end of file + +<%= render :partial => 'signature' %> diff --git a/app/views/notices/_atom_entry.html.haml b/app/views/notices/_atom_entry.html.haml index 7396862628..b11e19859d 100644 --- a/app/views/notices/_atom_entry.html.haml +++ b/app/views/notices/_atom_entry.html.haml @@ -6,13 +6,13 @@ = link_to(notice.request['url'], notice.request['url']) %p %strong Where: - = notice.err.where + = notice.err.where %p %strong Occured: = notice.created_at.to_s(:micro) %p %strong Similar: - = notice.err.notices.count - 1 + = notice.err.notices_count - 1 %h3 Params %p= pretty_hash(notice.params) diff --git a/app/views/notices/_environment.html.haml b/app/views/notices/_environment.html.haml index ebf533ef8b..8db2fb12b1 100644 --- a/app/views/notices/_environment.html.haml +++ b/app/views/notices/_environment.html.haml @@ -2,5 +2,5 @@ %table.environment - notice.env_vars.each do |key,val| %tr - %th= key + %th= raw key %td.main= val \ No newline at end of file diff --git a/app/views/notices/_summary.html.haml b/app/views/notices/_summary.html.haml index 06889e6912..4fe491c954 100644 --- a/app/views/notices/_summary.html.haml +++ b/app/views/notices/_summary.html.haml @@ -15,4 +15,4 @@ %td= notice.created_at.to_s(:micro) %tr %th Similar - %td= notice.err.notices.count - 1 \ No newline at end of file + %td= notice.err.notices_count - 1 \ No newline at end of file diff --git a/db/migrate/20110422152027_move_notices_to_separate_collection.rb b/db/migrate/20110422152027_move_notices_to_separate_collection.rb new file mode 100644 index 0000000000..f3ea60ee6b --- /dev/null +++ b/db/migrate/20110422152027_move_notices_to_separate_collection.rb @@ -0,0 +1,22 @@ +class MoveNoticesToSeparateCollection < Mongoid::Migration + def self.up + # copy embedded Notices into a separate collection + mongo_db = Err.db + errs = mongo_db.collection("errs").find({ }, :fields => ["notices"]) + errs.each do |err| + next unless err['notices'] + e = Err.find(err['_id']) + puts "Copying notices for Err #{err['_id']}" + err['notices'].each do |notice| + e.notices.create!(notice) + end + mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}}) + end + Rake::Task["errbit:db:update_notices_count"].invoke + Rake::Task["errbit:db:update_err_message"].invoke + end + + def self.down + end + +end diff --git a/lib/hoptoad.rb b/lib/hoptoad.rb index ee6d799022..4bf64e65ab 100644 --- a/lib/hoptoad.rb +++ b/lib/hoptoad.rb @@ -1,13 +1,13 @@ module Hoptoad module V2 require 'digest/md5' - + class ApiVersionError < StandardError def initialize super "Wrong API Version: Expecting v2.0" end end - + def self.parse_xml(xml) parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice'] raise ApiVersionError unless parsed && parsed['version'] == '2.0' @@ -15,9 +15,9 @@ def self.parse_xml(xml) rekeyed['fingerprint'] = Digest::MD5.hexdigest(rekeyed['error']['backtrace'].to_s) rekeyed end - + private - + def self.rekey(node) if node.is_a?(Hash) && node.has_key?('var') && node.has_key?('key') {node['key'] => rekey(node['var'])} @@ -42,4 +42,4 @@ def self.rekey(node) end end end -end \ No newline at end of file +end diff --git a/lib/recurse.rb b/lib/recurse.rb new file mode 100644 index 0000000000..7f5a4ee9a4 --- /dev/null +++ b/lib/recurse.rb @@ -0,0 +1,24 @@ +class Hash + + # Apply a block to hash, and recursively apply that block + # to each sub-hash or +types+. + # + # h = {:a=>1, :b=>{:b1=>1, :b2=>2}} + # g = h.recurse{|h| h.inject({}){|h,(k,v)| h[k.to_s] = v; h} } + # g #=> {"a"=>1, "b"=>{"b1"=>1, "b2"=>2}} + # + def recurse(*types, &block) + types = [self.class] if types.empty? + h = inject({}) do |hash, (key, value)| + case value + when *types + hash[key] = value.recurse(*types, &block) + else + hash[key] = value + end + hash + end + yield h + end + +end diff --git a/lib/tasks/errbit/err_message.rake b/lib/tasks/errbit/err_message.rake new file mode 100644 index 0000000000..6f9e8c17f7 --- /dev/null +++ b/lib/tasks/errbit/err_message.rake @@ -0,0 +1,12 @@ +namespace :errbit do + + namespace :db do + desc "Updates Err#notices_count" + task :update_err_message => :environment do + puts "Updating err.message" + Err.all.each do |e| + e.update_attributes(:message => e.notices.first.message) if e.notices.first + end + end + end +end diff --git a/lib/tasks/errbit/notices_counter.rake b/lib/tasks/errbit/notices_counter.rake new file mode 100644 index 0000000000..617ea447d3 --- /dev/null +++ b/lib/tasks/errbit/notices_counter.rake @@ -0,0 +1,12 @@ +namespace :errbit do + + namespace :db do + desc "Updates Err#notices_count" + task :update_notices_count => :environment do + puts "Updating err.notices_count" + Err.all.each do |e| + e.update_attributes(:notices_count => e.notices.count) + end + end + end +end diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 86f8fb4051..3d3149be0f 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe ErrsController do - + it_requires_authentication :for => { :index => :get, :all => :get, :show => :get, :resolve => :put }, :params => {:app_id => 'dummyid', :id => 'dummyid'} - + let(:app) { Factory(:app) } let(:err) { Factory(:err, :app => app) } - + describe "GET /errs" do render_views context 'when logged in as an admin' do @@ -31,7 +31,7 @@ response.should be_success response.body.should match(@err.message) end - + it "should handle lots of errors" do pending "Turning off long running spec" 1000.times { Factory :notice } @@ -55,7 +55,7 @@ end end end - + context 'when logged in as a user' do it 'gets a paginated list of unresolved errs for the users apps' do sign_in(user = Factory(:user)) @@ -68,7 +68,7 @@ end end end - + describe "GET /errs/all" do context 'when logged in as an admin' do it "gets a paginated list of all errs" do @@ -83,7 +83,7 @@ assigns(:errs).should == errs end end - + context 'when logged in as a user' do it 'gets a paginated list of all errs for the users apps' do sign_in(user = Factory(:user)) @@ -96,29 +96,29 @@ end end end - + describe "GET /apps/:app_id/errs/:id" do render_views - + before do 3.times { Factory(:notice, :err => err)} end - + context 'when logged in as an admin' do before do sign_in Factory(:admin) end - + it "finds the app" do get :show, :app_id => app.id, :id => err.id assigns(:app).should == app end - + it "finds the err" do get :show, :app_id => app.id, :id => err.id assigns(:err).should == err end - + it "successfully render page" do get :show, :app_id => app.id, :id => err.id response.should be_success @@ -131,9 +131,9 @@ err = Factory :err get :show, :app_id => err.app.id, :id => err.id - response.body.should_not button_matcher + response.body.should_not button_matcher end - + it "should exist for err's app with issue tracker" do tracker = Factory(:lighthouseapp_tracker) err = Factory(:err, :app => tracker.app) @@ -141,7 +141,7 @@ response.body.should button_matcher end - + it "should not exist for err with issue_link" do tracker = Factory(:lighthouseapp_tracker) err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host") @@ -151,7 +151,7 @@ end end end - + context 'when logged in as a user' do before do sign_in(@user = Factory(:user)) @@ -160,12 +160,12 @@ @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app) @watched_err = Factory(:err, :app => @watched_app) end - + it 'finds the err if the user is watching the app' do get :show, :app_id => @watched_app.to_param, :id => @watched_err.id assigns(:err).should == @watched_err end - + it 'raises a DocumentNotFound error if the user is not watching the app' do lambda { get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id @@ -173,17 +173,17 @@ end end end - + describe "PUT /apps/:app_id/errs/:id/resolve" do before do sign_in Factory(:admin) - + @err = Factory(:err) App.stub(:find).with(@err.app.id).and_return(@err.app) @err.app.errs.stub(:find).and_return(@err) @err.stub(:resolve!) end - + it 'finds the app and the err' do App.should_receive(:find).with(@err.app.id).and_return(@err.app) @err.app.errs.should_receive(:find).and_return(@err) @@ -191,17 +191,17 @@ assigns(:app).should == @err.app assigns(:err).should == @err end - + it "should resolve the issue" do @err.should_receive(:resolve!).and_return(true) put :resolve, :app_id => @err.app.id, :id => @err.id end - + it "should display a message" do put :resolve, :app_id => @err.app.id, :id => @err.id request.flash[:success].should match(/Great news/) end - + it "should redirect to the app page" do put :resolve, :app_id => @err.app.id, :id => @err.id response.should redirect_to(app_path(@err.app)) diff --git a/spec/models/err_spec.rb b/spec/models/err_spec.rb index 95f68e90dd..5eaf124169 100644 --- a/spec/models/err_spec.rb +++ b/spec/models/err_spec.rb @@ -1,21 +1,21 @@ require 'spec_helper' describe Err do - + context 'validations' do it 'requires a klass' do err = Factory.build(:err, :klass => nil) err.should_not be_valid err.errors[:klass].should include("can't be blank") end - + it 'requires an environment' do err = Factory.build(:err, :environment => nil) err.should_not be_valid err.errors[:environment].should include("can't be blank") end end - + context '#for' do before do @app = Factory(:app) @@ -27,16 +27,16 @@ :environment => 'production' } end - + it 'returns the correct err if one already exists' do existing = Err.create(@conditions) Err.for(@conditions).should == existing end - + it 'assigns the returned err to the given app' do Err.for(@conditions).app.should == @app end - + it 'creates a new err if a matching one does not already exist' do Err.where(@conditions.except(:app)).exists?.should == false lambda { @@ -44,36 +44,47 @@ }.should change(Err,:count).by(1) end end - + context '#last_notice_at' do it "returns the created_at timestamp of the latest notice" do err = Factory(:err) err.last_notice_at.should be_nil - + notice1 = Factory(:notice, :err => err) err.last_notice_at.should == notice1.created_at - + notice2 = Factory(:notice, :err => err) err.last_notice_at.should == notice2.created_at end end - + context '#message' do + it "returns klass by default" do + err = Factory(:err) + err.message.should == err.klass + end + it 'returns the message from the first notice' do err = Factory(:err) notice1 = Factory(:notice, :err => err, :message => 'ERR 1') notice2 = Factory(:notice, :err => err, :message => 'ERR 2') err.message.should == notice1.message end + + it "adding a notice caches its message" do + err = Factory(:err) + lambda { + notice1 = Factory(:notice, :err => err, :message => 'ERR 1')}.should change(err, :message).from(err.klass).to('ERR 1') + end end - + context "#resolved?" do it "should start out as unresolved" do err = Err.new err.should_not be_resolved err.should be_unresolved end - + it "should be able to be resolved" do err = Factory(:err) err.should_not be_resolved @@ -81,7 +92,7 @@ err.reload.should be_resolved end end - + context "resolve!" do it "marks the err as resolved" do err = Factory(:err) @@ -89,7 +100,7 @@ err.resolve! err.should be_resolved end - + it "should throw an err if it's not successful" do err = Factory(:err) err.should_not be_resolved @@ -100,7 +111,7 @@ }.should raise_error(Mongoid::Errors::Validations) end end - + context "Scopes" do context "resolved" do it 'only finds resolved Errs' do @@ -110,7 +121,7 @@ Err.resolved.all.should_not include(unresolved) end end - + context "unresolved" do it 'only finds unresolved Errs' do resolved = Factory(:err, :resolved => true) @@ -130,4 +141,30 @@ end end end -end \ No newline at end of file + + context "notice counter cache" do + + before do + @app = Factory(:app) + @err = Factory(:err, :app => @app) + end + + it "#notices_count returns 0 by default" do + @err.notices_count.should == 0 + end + + it "adding a notice increases #notices_count by 1" do + lambda { + notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')}.should change(@err, :notices_count).from(0).to(1) + end + + it "removing a notice decreases #notices_count by 1" do + notice1 = Factory(:notice, :err => @err, :message => 'ERR 1') + lambda { + @err.notices.first.destroy + }.should change(@err, :notices_count).from(1).to(0) + end + end + + +end diff --git a/spec/models/notice_spec.rb b/spec/models/notice_spec.rb index 2d89542107..07920b3096 100644 --- a/spec/models/notice_spec.rb +++ b/spec/models/notice_spec.rb @@ -1,39 +1,39 @@ require 'spec_helper' describe Notice do - + context 'validations' do it 'requires a backtrace' do notice = Factory.build(:notice, :backtrace => nil) notice.should_not be_valid notice.errors[:backtrace].should include("can't be blank") end - + it 'requires the server_environment' do notice = Factory.build(:notice, :server_environment => nil) notice.should_not be_valid notice.errors[:server_environment].should include("can't be blank") end - + it 'requires the notifier' do notice = Factory.build(:notice, :notifier => nil) notice.should_not be_valid notice.errors[:notifier].should include("can't be blank") end end - + context '#from_xml' do before do @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read @app = Factory(:app, :api_key => 'APIKEY') Digest::MD5.stub(:hexdigest).and_return('fingerprintdigest') end - + it 'finds the correct app' do @notice = Notice.from_xml(@xml) @notice.err.app.should == @app end - + it 'finds the correct err for the notice' do Err.should_receive(:for).with({ :app => @app, @@ -46,7 +46,7 @@ err.notices.stub(:create!) @notice = Notice.from_xml(@xml) end - + it 'marks the err as unresolve if it was previously resolved' do Err.should_receive(:for).with({ :app => @app, @@ -61,56 +61,70 @@ @notice.err.should == err @notice.err.should_not be_resolved end - + it 'should create a new notice' do @notice = Notice.from_xml(@xml) @notice.should be_persisted end - + it 'assigns an err to the notice' do @notice = Notice.from_xml(@xml) @notice.err.should be_a(Err) end - + it 'captures the err message' do @notice = Notice.from_xml(@xml) @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.' end - + it 'captures the backtrace' do @notice = Notice.from_xml(@xml) @notice.backtrace.size.should == 73 @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake' end - + it 'captures the server_environment' do @notice = Notice.from_xml(@xml) @notice.server_environment['environment-name'].should == 'development' end - + it 'captures the request' do @notice = Notice.from_xml(@xml) @notice.request['url'].should == 'http://example.org/verify' @notice.request['params']['controller'].should == 'application' end - + it 'captures the notifier' do @notice = Notice.from_xml(@xml) @notice.notifier['name'].should == 'Hoptoad Notifier' end - it "should handle params withour 'request' section" do + it "should handle params without 'request' section" do @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read lambda { Notice.from_xml(@xml) }.should_not raise_error end end - + + describe "key sanitization" do + before do + @hash = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}} + @hash_sanitized = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}} + end + [:server_environment, :request, :notifier].each do |key| + it "replaces . with . and $ with $ in keys used in #{key}" do + err = Factory(:err) + notice = Factory(:notice, :err => err, key => @hash) + notice.send(key).should == @hash_sanitized + end + end + end + describe "email notifications" do before do @app = Factory(:app_with_watcher) @err = Factory(:err, :app => @app) end - + Errbit::Config.email_at_notices.each do |threshold| it "sends an email notification after #{threshold} notice(s)" do @err.notices.stub(:count).and_return(threshold) @@ -120,5 +134,5 @@ end end end - -end \ No newline at end of file + +end From 567d7e96dc171dcda88a1d2325ea0b43e36e2e40 Mon Sep 17 00:00:00 2001 From: Bob Lail <bob.lailfamily@gmail.com> Date: Sat, 23 Apr 2011 13:10:25 -0500 Subject: [PATCH 102/115] app names are links --- app/views/errs/_table.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/errs/_table.html.haml b/app/views/errs/_table.html.haml index 28efc14f37..c5df79ff10 100644 --- a/app/views/errs/_table.html.haml +++ b/app/views/errs/_table.html.haml @@ -11,7 +11,7 @@ - errs.each do |err| %tr{:class => err.resolved? ? 'resolved' : 'unresolved'} %td.app - = err.app.name + = link_to err.app.name, app_path(err.app) %span.environment= err.environment %td.message = link_to err.message, app_err_path(err.app, err) From a02dacae093789471b601c378f6015690a527fc5 Mon Sep 17 00:00:00 2001 From: Ben Langfeld <ben@langfeld.me> Date: Mon, 25 Apr 2011 21:25:02 +0100 Subject: [PATCH 103/115] Add support for Pivotal Tracker issue creation --- Gemfile | 1 + Gemfile.lock | 11 +++ README.md | 10 ++- app/helpers/application_helper.rb | 8 ++ app/models/app.rb | 20 ++--- app/models/issue_tracker.rb | 37 ++++++-- app/views/apps/_fields.html.haml | 15 ++-- app/views/errs/pivotal_body.txt.erb | 21 +++++ public/javascripts/form.js | 21 ++--- public/stylesheets/application.css | 102 ++++++++++++---------- spec/controllers/apps_controller_spec.rb | 76 ++++++++++------ spec/controllers/errs_controller_spec.rb | 83 ++++++++++++------ spec/factories/issue_tracker_factories.rb | 17 ++-- 13 files changed, 280 insertions(+), 142 deletions(-) create mode 100644 app/views/errs/pivotal_body.txt.erb diff --git a/Gemfile b/Gemfile index f0f9cc63a3..859199a72b 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'will_paginate' gem 'devise', '~> 1.1.8' gem 'lighthouse-api' gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" +gem 'pivotal-tracker' platform :ruby do gem 'bson_ext', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 69b33db007..f5ea79dda9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,7 +54,10 @@ GEM factory_girl (~> 1.3) railties (>= 3.0.0) haml (3.0.25) + happymapper (0.3.2) + libxml-ruby (~> 1.1.3) i18n (0.5.0) + libxml-ruby (1.1.4) lighthouse-api (2.0) activeresource (>= 3.0.0) activesupport (>= 3.0.0) @@ -72,6 +75,11 @@ GEM tzinfo (~> 0.3.22) will_paginate (~> 3.0.pre) nokogiri (1.4.4) + pivotal-tracker (0.2.0) + builder + happymapper (>= 0.2.4) + nokogiri (~> 1.4.1) + rest-client (~> 1.5.1) polyglot (0.3.1) rack (1.2.2) rack-mount (0.6.13) @@ -92,6 +100,8 @@ GEM rake (>= 0.8.7) thor (~> 0.14.4) rake (0.8.7) + rest-client (1.5.1) + mime-types (>= 1.16) rspec (2.5.0) rspec-core (~> 2.5.0) rspec-expectations (~> 2.5.0) @@ -128,6 +138,7 @@ DEPENDENCIES lighthouse-api mongoid (~> 2.0.0.rc.7) nokogiri + pivotal-tracker rails (= 3.0.5) redmine_client! rspec (~> 2.5) diff --git a/README.md b/README.md index e20a020d0a..23714f4dc6 100644 --- a/README.md +++ b/README.md @@ -92,17 +92,23 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at Lighthouseapp integration ------------------------- -* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview +* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview * Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it. * Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview Redmine integration ------------------------- -* Account is the host of your redmine installation, i.e. **http://redmine.org** +* Account is the host of your redmine installation, i.e. **http://redmine.org** * Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it. * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject +Pivotal Tracker integration +------------------------- + +* Errbit uses token-based authentication. Get your API Key or visit [http://www.pivotaltracker.com/help/api](http://www.pivotaltracker.com/help/api) to learn how to get it. +* Project id is an identifier of your project, i.e. **24324** for project at http://www.pivotaltracker.com/projects/24324 + TODO ---- diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a5ba883fe8..9edce4b4da 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,4 +2,12 @@ module ApplicationHelper def lighthouse_tracker? object object.issue_tracker_type == "lighthouseapp" end + + def redmine_tracker? object + object.issue_tracker_type == "redmine" + end + + def pivotal_tracker? object + object.issue_tracker_type == "pivotal" + end end diff --git a/app/models/app.rb b/app/models/app.rb index 3709ae13ff..c3098a1c21 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -1,7 +1,7 @@ class App include Mongoid::Document include Mongoid::Timestamps - + field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false @@ -21,29 +21,29 @@ class App embeds_many :deploys embeds_one :issue_tracker references_many :errs, :dependent => :destroy - + before_validation :generate_api_key, :on => :create - + validates_presence_of :name, :api_key validates_uniqueness_of :name, :allow_blank => true validates_uniqueness_of :api_key, :allow_blank => true validates_associated :watchers validate :check_issue_tracker - + accepts_nested_attributes_for :watchers, :allow_destroy => true, :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, - :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) } - + :reject_if => proc { |attrs| !%w(lighthouseapp redmine pivotal).include?(attrs[:issue_tracker_type]) } + # Mongoid Bug: find(id) on association proxies returns an Enumerator def self.find_by_id!(app_id) where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id)) end - + def self.find_by_api_key!(key) where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key)) end - + def last_deploy_at deploys.last && deploys.last.created_at end @@ -58,9 +58,9 @@ def notify_on_deploys !(self[:notify_on_deploys] == false) end alias :notify_on_deploys? :notify_on_deploys - + protected - + def generate_api_key self.api_key ||= ActiveSupport::SecureRandom.hex end diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb index 717cd37594..7a6dc235eb 100644 --- a/app/models/issue_tracker.rb +++ b/app/models/issue_tracker.rb @@ -6,7 +6,7 @@ class IssueTracker default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host] validate :check_params - + embedded_in :app, :inverse_of => :issue_tracker field :account, :type => String @@ -15,8 +15,14 @@ class IssueTracker field :issue_tracker_type, :type => String, :default => 'lighthouseapp' def create_issue err - return create_lighthouseapp_issue err if issue_tracker_type == 'lighthouseapp' - create_redmine_issue err if issue_tracker_type == 'redmine' + case issue_tracker_type + when 'lighthouseapp' + create_lighthouseapp_issue err + when 'redmine' + create_redmine_issue err + when 'pivotal' + create_pivotal_issue err + end end protected @@ -34,6 +40,14 @@ def create_redmine_issue err err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}") end + def create_pivotal_issue err + PivotalTracker::Client.token = api_token + PivotalTracker::Client.use_ssl = true + project = PivotalTracker::Project.find project_id.to_i + story = project.stories.create :name => issue_title(err), :story_type => 'bug', :description => self.class.pivotal_body_template.result(binding) + err.update_attribute :issue_link, "https://www.pivotaltracker.com/story/show/#{story.id}" + end + def create_lighthouseapp_issue err Lighthouse.account = account Lighthouse.token = api_token @@ -56,12 +70,17 @@ def issue_title err end def check_params - blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } + blank_flag_fields = %w(api_token project_id) + blank_flag_fields << 'account' if %w(lighthouseapp redmine).include? issue_tracker_type + blank_flags = blank_flag_fields.map {|m| self[m].blank? } if blank_flags.any? && !blank_flags.all? - message = if issue_tracker_type == 'lighthouseapp' + message = case issue_tracker_type + when 'lighthouseapp' "You must specify your Lighthouseapp account, api token and project id" - else + when 'redmine' "You must specify your Redmine url, api token and project id" + when 'pivotal' + "You must specify your Pivotal Tracker api token and project id" end errors.add(:base, message) end @@ -71,9 +90,13 @@ class << self def lighthouseapp_body_template @@lighthouseapp_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, '')) end - + def redmine_body_template @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb")) end + + def pivotal_body_template + @@pivotal_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/pivotal_body.txt.erb")) + end end end diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index d9a2793fe2..43a2267382 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -3,7 +3,7 @@ %div.required = f.label :name = f.text_field :name - + %div.checkbox = f.check_box :notify_on_errs = f.label :notify_on_errs, 'Notify on errors' @@ -39,19 +39,24 @@ = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp') = w.radio_button :issue_tracker_type, :redmine = label_tag :issue_tracker_type_redmine, 'Redmine', :for => label_for_attr(w, 'issue_tracker_type_redmine') - %div.tracker_params{:class => lighthouse_tracker?(w.object) ? 'choosen' : nil} + = w.radio_button :issue_tracker_type, :pivotal + = label_tag :issue_tracker_type_pivotal, 'Pivotal Tracker', :for => label_for_attr(w, 'issue_tracker_type_pivotal') + %div.tracker_params.lighthouseapp{:class => lighthouse_tracker?(w.object) ? 'chosen' : nil} = w.label :account, "Account" = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" = w.label :api_token, "API token" = w.text_field :api_token, :placeholder => "API Token for your account" = w.label :project_id, "Project ID" = w.text_field :project_id, :placeholder => "123 from abc from abc.lighthouseapp.com/projects/123" - %div.tracker_params{:class => lighthouse_tracker?(w.object) ? nil : 'choosen'} + %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil} = w.label :account, "Redmine URL" = w.text_field :account, :placeholder => "like http://www.redmine.org/" = w.label :api_token, "API token" = w.text_field :api_token, :placeholder => "API Token for your account" = w.label :project_id, "Project ID" = w.text_field :project_id - - + %div.tracker_params.pivotal{:class => pivotal_tracker?(w.object) ? 'chosen' : nil} + = w.label :project_id, "Project ID" + = w.text_field :project_id + = w.label :api_token, "API token" + = w.text_field :api_token, :placeholder => "API Token for your account" diff --git a/app/views/errs/pivotal_body.txt.erb b/app/views/errs/pivotal_body.txt.erb new file mode 100644 index 0000000000..d820ea48f5 --- /dev/null +++ b/app/views/errs/pivotal_body.txt.erb @@ -0,0 +1,21 @@ +See this exception on Errbit: <%= app_err_url err.app, err %> +<% if notice = err.notices.first %> + <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %> + Where: <%= notice.err.where %> + Occurred: <%= notice.created_at.to_s :micro %> + Similar: <%= (notice.err.notices.count - 1).to_s %> + + Params: + <%= pretty_hash notice.params %> + + Session: + <%= pretty_hash notice.session %> + + Backtrace: + <%= notice.backtrace.map { |line| "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %> + + Environment: + <% notice.env_vars.each do |key, val| %> + <%= "#{key}: #{val}" %> + <% end %> +<% end %> diff --git a/public/javascripts/form.js b/public/javascripts/form.js index b2e1eefef8..b0f6c64dea 100644 --- a/public/javascripts/form.js +++ b/public/javascripts/form.js @@ -1,6 +1,6 @@ $(function(){ activateNestedForms(); - + if($('div.watcher.nested').length) activateWatcherTypeSelector(); @@ -11,9 +11,9 @@ $(function(){ function activateNestedForms() { $('.nested-wrapper').each(function(){ var wrapper = $(this); - + makeNestedItemsDestroyable(wrapper); - + var addLink = $('<a/>').text('add another').addClass('add-nested'); addLink.click(appendNestedItem); wrapper.append(addLink); @@ -35,7 +35,7 @@ function appendNestedItem() { var nestedItem = addLink.parent().find('.nested').first().clone().show(); var timestamp = new Date(); timestamp = timestamp.valueOf(); - + nestedItem.find('input, select').each(function(){ var input = $(this); input.attr('id', input.attr('id').replace(/([_\[])\d+([\]_])/,'$1'+timestamp+'$2')); @@ -73,17 +73,10 @@ function activateWatcherTypeSelector() { } function activateIssueTrackerTypeSelector() { - var not_choosen = $("div.tracker_params").filter(function () { - return !$(this).hasClass("choosen"); - }); - window.hiddenTracker = not_choosen.html(); - not_choosen.remove(); $('div.issue_tracker input[name*=issue_tracker_type]').live('click', function(){ - var choosen = $(this).val(); + var chosen = $(this).val(); var wrapper = $(this).closest('.nested'); - var tmp; - tmp = wrapper.find('div.choosen').html(); - wrapper.find('div.choosen').html(window.hiddenTracker); - window.hiddenTracker = tmp; + wrapper.find('div.chosen').removeClass('chosen'); + wrapper.find('div.'+chosen).addClass('chosen'); }); } \ No newline at end of file diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 41ac34038b..9d51f99314 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -1,9 +1,9 @@ -html { +html { margin: 0; padding: 0; color: #585858; background-color: #E2E2E2; font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif; } -body { +body { margin: 0; padding: 0; font-size: 1.3em; line-height: 1.4em; } @@ -34,7 +34,7 @@ a:visited { color: #0069cc;} a:hover { color: #0069cc; text-decoration: underline; } a.action { float: right; font-size: 0.9em;} -#header > div, #nav-bar, #content-wrapper, #footer { +#header > div, #nav-bar, #content-wrapper, #footer { width: 930px; margin: 0 auto; position: relative; @@ -98,19 +98,19 @@ a.action { float: right; font-size: 0.9em;} margin-bottom: 24px; height: 41px; } -#nav-bar li { - float: left; +#nav-bar li { + float: left; margin-right: 18px; color: #666; background: #FFF url(images/button-bg.png) 0 bottom repeat-x; border-radius: 50px; -moz-border-radius: 50px; -webkit-border-radius: 50px; - border: 1px solid #bbb; + border: 1px solid #bbb; } #nav-bar li a { color: #666; - display: block; + display: block; padding: 0 20px 0 40px; font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none; text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF; @@ -120,17 +120,17 @@ a.action { float: right; font-size: 0.9em;} #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); } #nav-bar li.errs a { background-image: url(images/icons/error.png); } #nav-bar li.users a { background-image: url(images/icons/user.png); } -#nav-bar li:hover { +#nav-bar li:hover { box-shadow: 0 0 3px #69c; -moz-box-shadow: 0 0 3px #69c; -webkit-box-shadow: 0 0 3px #69c; } -#nav-bar li.active { - border-color: #fff; +#nav-bar li.active { + border-color: #fff; background-color: #CCC; background-image: none; - box-shadow: inset 0 0 5px #999; - -moz-box-shadow: inset 0 0 5px #999; + box-shadow: inset 0 0 5px #999; + -moz-box-shadow: inset 0 0 5px #999; -webkit-box-shadow: inset 0 0 5px #999; } @@ -141,13 +141,13 @@ a.action { float: right; font-size: 0.9em;} /* Content Title */ #content-title { - padding: 30px 20px; + padding: 30px 20px; border-top: 1px solid #FFF; border-bottom: 1px solid #FFF; background-color: #e2e2e2; } #content-title h1 { - padding: 0; margin: 0; + padding: 0; margin: 0; width: 85%; border: none; color: #666; @@ -161,7 +161,7 @@ a.action { float: right; font-size: 0.9em;} position: absolute; top: 25px; right: 20px; } -#action-bar span { +#action-bar span { display: inline-block; margin-left: 18px; text-decoration: none; @@ -174,14 +174,14 @@ a.action { float: right; font-size: 0.9em;} } #action-bar span a { color: #666; - display: block; + display: block; padding: 0 20px 0 40px; font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none; text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF; background: transparent 10px 8px no-repeat; } #action-bar a:hover { text-decoration: none;} -#action-bar span:hover { +#action-bar span:hover { box-shadow: 0 0 3px #69c; -moz-box-shadow: 0 0 3px #69c; -webkit-box-shadow: 0 0 3px #69c; @@ -194,14 +194,14 @@ a.action { float: right; font-size: 0.9em;} #content { padding: 20px; border-top: 1px solid #C6C6C6; background-color: #FFF; -} +} -#content a.button { +#content a.button { float: right; display: block; margin-bottom: 10px; } - + /* Footer */ #footer { padding: 20px 0; @@ -211,22 +211,22 @@ a.action { float: right; font-size: 0.9em;} /* Flash Messages */ #flash-messages li { - padding: 13px 45px; - margin-bottom:25px; + padding: 13px 45px; + margin-bottom:25px; border: 1px solid #C6C6C6; background-color: #F9F9F9; line-height: 1em; } #flash-messages li.notice { - padding-left: 20px; + padding-left: 20px; background-color: #b5eeff; border: 1px solid #6cf; } -#flash-messages li.success { +#flash-messages li.success { background: #cfc url(images/icons/success.png) 16px 50% no-repeat; border: 1px solid #6c3; } -#flash-messages li.error { +#flash-messages li.error { background: #fcc url(images/icons/error.png) 16px 50% no-repeat; border: 1px solid #f99; } @@ -244,13 +244,13 @@ form fieldset { padding: 0.8em; margin-bottom: 1em; background-color: #F0F0F0; border: 1px solid #C6C6C6; border-left: none; border-right: none; } -form fieldset legend { - font-size: 1.2em; font-weight: bold; text-transform: uppercase; +form fieldset legend { + font-size: 1.2em; font-weight: bold; text-transform: uppercase; color: #555; } form label { font-weight: bold; text-transform: uppercase; line-height: 1.6em; - display: inline-block; + display: inline-block; } form label.inline { display: inline; } form .checkbox label { display: inline; } @@ -281,17 +281,17 @@ form input[type=submit] { font-size: 1.2em; line-height: 1em; text-transform: uppercase; border: none; color: #FFF; background-color: #387fc1; } -form div.buttons { +form div.buttons { color: #666; background: #FFF url(images/button-bg.png) 0 bottom repeat-x; border-radius: 50px; -moz-border-radius: 50px; -webkit-border-radius: 50px; - border: 1px solid #bbb; + border: 1px solid #bbb; display: inline-block; } -form div.buttons:hover { - color: #666; +form div.buttons:hover { + color: #666; box-shadow: 0 0 3px #69c; -moz-box-shadow: 0 0 3px #69c; -webkit-box-shadow: 0 0 3px #69c; @@ -351,10 +351,10 @@ form .error-messages ul { } /* Tables */ -table { - width: 100%; +table { + width: 100%; border: 1px solid #C6C6C6; - margin-bottom: 1.5em; + margin-bottom: 1.5em; border-collapse: separate; } table thead th { @@ -364,10 +364,10 @@ table thead th { table tbody tr:first-child td { border-top: 1px solid #C6C6C6; } -table th, table td { - border-top: 1px solid #C6C6C6; - padding: 10px 8px; - text-align: left; +table th, table td { + border-top: 1px solid #C6C6C6; + padding: 10px 8px; + text-align: left; } table th { background-color: #E2E2E2; font-weight: bold; text-transform: uppercase; white-space: nowrap; } table tbody tr:nth-child(odd) td { background-color: #F9F9F9; } @@ -442,8 +442,8 @@ pre { background-color: #CCC; background-image: none; border-color: #FFF; - box-shadow: inset 0 0 5px #999; - -moz-box-shadow: inset 0 0 5px #999; + box-shadow: inset 0 0 5px #999; + -moz-box-shadow: inset 0 0 5px #999; -webkit-box-shadow: inset 0 0 5px #999; font-style: normal; } @@ -477,11 +477,11 @@ a:hover.button { background-color: #eee; } a.button.active { - border-color: #fff; + border-color: #fff; background-color: #CCC; background-image: none; - box-shadow: inset 0 0 5px #999; - -moz-box-shadow: inset 0 0 5px #999; + box-shadow: inset 0 0 5px #999; + -moz-box-shadow: inset 0 0 5px #999; -webkit-box-shadow: inset 0 0 5px #999; } @@ -502,10 +502,10 @@ a.button.active { } /* Watchers and Issue Tracker Forms */ -div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .lighthouseapp, div.issue_tracker.nested .redmine { +div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .lighthouseapp, div.issue_tracker.nested .redmine, div.issue_tracker.nested .pivotal { display: none; } -div.nested.watcher .choosen, div.nested.issue_tracker .choosen { +div.nested.watcher .choosen, div.nested.issue_tracker .chosen { display: block; } @@ -559,7 +559,7 @@ table.errs td.app .environment { font-size: 0.8em; color: #999; } -table.errs td.message a { +table.errs td.message a { width: 420px; display: block; word-wrap: break-word; @@ -587,6 +587,10 @@ table.errs tr.resolved td > * { background: transparent url(/images/redmine_create.png) 6px 5px no-repeat; } +#action-bar a.pivotal_create { + background: transparent url(/images/pivotal_create.png) 6px 5px no-repeat; +} + #action-bar a.lighthouseapp_goto { background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat; } @@ -595,6 +599,10 @@ table.errs tr.resolved td > * { background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat; } +#action-bar a.pivotal_goto { + background: transparent url(/images/pivotal_goto.png) 6px 5px no-repeat; +} + /* Notices Pagination */ .notice-pagination { float: left; diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 38eb9c7e30..dee4236f3b 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -5,7 +5,7 @@ it_requires_authentication it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete} - + describe "GET /apps" do context 'when logged in as an admin' do it 'finds all apps' do @@ -16,7 +16,7 @@ assigns(:apps).should == apps end end - + context 'when logged in as a regular user' do it 'finds apps the user is watching' do sign_in(user = Factory(:user)) @@ -31,7 +31,7 @@ end end end - + describe "GET /apps/:id" do context 'logged in as an admin' do before(:each) do @@ -75,27 +75,27 @@ end end end - + context 'logged in as a user' do it 'finds the app if the user is watching it' do pending end - + it 'does not find the app if the user is not watching it' do sign_in Factory(:user) app = Factory(:app) - lambda { + lambda { get :show, :id => app.id }.should raise_error(Mongoid::Errors::DocumentNotFound) end end end - + context 'logged in as an admin' do before do sign_in Factory(:admin) end - + describe "GET /apps/new" do it 'instantiates a new app with a prebuilt watcher' do get :new @@ -104,7 +104,7 @@ assigns(:app).watchers.should_not be_empty end end - + describe "GET /apps/:id/edit" do it 'finds the correct app' do app = Factory(:app) @@ -112,29 +112,29 @@ assigns(:app).should == app end end - + describe "POST /apps" do before do @app = Factory(:app) App.stub(:new).and_return(@app) end - + context "when the create is successful" do before do @app.should_receive(:save).and_return(true) end - + it "should redirect to the app page" do post :create, :app => {} response.should redirect_to(app_path(@app)) end - + it "should display a message" do post :create, :app => {} request.flash[:success].should match(/success/) end end - + context "when the create is unsuccessful" do it "should render the new page" do @app.should_receive(:save).and_return(false) @@ -143,18 +143,18 @@ end end end - + describe "PUT /apps/:id" do before do @app = Factory(:app) end - + context "when the update is successful" do it "should redirect to the app page" do put :update, :id => @app.id, :app => {} response.should redirect_to(app_path(@app)) end - + it "should display a message" do put :update, :id => @app.id, :app => {} request.flash[:success].should match(/success/) @@ -168,7 +168,7 @@ response.should redirect_to(app_path(id)) end end - + context "when the update is unsuccessful" do it "should render the edit page" do put :update, :id => @app.id, :app => { :name => '' } @@ -179,7 +179,7 @@ context "setting up issue tracker", :cur => true do context "unknown tracker type" do before(:each) do - put :update, :id => @app.id, :app => { :issue_tracker_attributes => { + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { :issue_tracker_type => 'unknown', :project_id => '1234', :api_token => '123123', :account => 'myapp' } } @app.reload @@ -211,7 +211,7 @@ @app.reload @app.issue_tracker.should be_nil - response.body.should match(/You must specify your Lighthouseapp account, api token and project id/) + response.body.should match(/You must specify your Lighthouseapp account, api token and project id/) end end @@ -236,38 +236,60 @@ @app.reload @app.issue_tracker.should be_nil - response.body.should match(/You must specify your Redmine url, api token and project id/) + response.body.should match(/You must specify your Redmine url, api token and project id/) + end + end + + context "pivotal" do + it "should save tracker params" do + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { + :issue_tracker_type => 'pivotal', :project_id => '1234', :api_token => '123123' } } + @app.reload + + tracker = @app.issue_tracker + tracker.issue_tracker_type.should == 'pivotal' + tracker.project_id.should == '1234' + tracker.api_token.should == '123123' + end + + it "should show validation notice when sufficient params are not present" do + put :update, :id => @app.id, :app => { :issue_tracker_attributes => { + :issue_tracker_type => 'pivotal', :project_id => '1234', :api_token => '123123' } } + @app.reload + + @app.issue_tracker.should be_nil + response.body.should match(/You must specify your Pivotal Tracker api token and project id/) end end end end - + describe "DELETE /apps/:id" do before do @app = Factory(:app) App.stub(:find).with(@app.id).and_return(@app) end - + it "should find the app" do delete :destroy, :id => @app.id assigns(:app).should == @app end - + it "should destroy the app" do @app.should_receive(:destroy) delete :destroy, :id => @app.id end - + it "should display a message" do delete :destroy, :id => @app.id request.flash[:success].should match(/success/) end - + it "should redirect to the apps page" do delete :destroy, :id => @app.id response.should redirect_to(apps_path) end end end - + end diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index 86f8fb4051..283a0671c6 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe ErrsController do - + it_requires_authentication :for => { :index => :get, :all => :get, :show => :get, :resolve => :put }, :params => {:app_id => 'dummyid', :id => 'dummyid'} - + let(:app) { Factory(:app) } let(:err) { Factory(:err, :app => app) } - + describe "GET /errs" do render_views context 'when logged in as an admin' do @@ -31,7 +31,7 @@ response.should be_success response.body.should match(@err.message) end - + it "should handle lots of errors" do pending "Turning off long running spec" 1000.times { Factory :notice } @@ -55,7 +55,7 @@ end end end - + context 'when logged in as a user' do it 'gets a paginated list of unresolved errs for the users apps' do sign_in(user = Factory(:user)) @@ -68,7 +68,7 @@ end end end - + describe "GET /errs/all" do context 'when logged in as an admin' do it "gets a paginated list of all errs" do @@ -83,7 +83,7 @@ assigns(:errs).should == errs end end - + context 'when logged in as a user' do it 'gets a paginated list of all errs for the users apps' do sign_in(user = Factory(:user)) @@ -96,29 +96,29 @@ end end end - + describe "GET /apps/:app_id/errs/:id" do render_views - + before do 3.times { Factory(:notice, :err => err)} end - + context 'when logged in as an admin' do before do sign_in Factory(:admin) end - + it "finds the app" do get :show, :app_id => app.id, :id => err.id assigns(:app).should == app end - + it "finds the err" do get :show, :app_id => app.id, :id => err.id assigns(:err).should == err end - + it "successfully render page" do get :show, :app_id => app.id, :id => err.id response.should be_success @@ -131,9 +131,9 @@ err = Factory :err get :show, :app_id => err.app.id, :id => err.id - response.body.should_not button_matcher + response.body.should_not button_matcher end - + it "should exist for err's app with issue tracker" do tracker = Factory(:lighthouseapp_tracker) err = Factory(:err, :app => tracker.app) @@ -141,7 +141,7 @@ response.body.should button_matcher end - + it "should not exist for err with issue_link" do tracker = Factory(:lighthouseapp_tracker) err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host") @@ -151,7 +151,7 @@ end end end - + context 'when logged in as a user' do before do sign_in(@user = Factory(:user)) @@ -160,12 +160,12 @@ @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app) @watched_err = Factory(:err, :app => @watched_app) end - + it 'finds the err if the user is watching the app' do get :show, :app_id => @watched_app.to_param, :id => @watched_err.id assigns(:err).should == @watched_err end - + it 'raises a DocumentNotFound error if the user is not watching the app' do lambda { get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id @@ -173,17 +173,17 @@ end end end - + describe "PUT /apps/:app_id/errs/:id/resolve" do before do sign_in Factory(:admin) - + @err = Factory(:err) App.stub(:find).with(@err.app.id).and_return(@err.app) @err.app.errs.stub(:find).and_return(@err) @err.stub(:resolve!) end - + it 'finds the app and the err' do App.should_receive(:find).with(@err.app.id).and_return(@err.app) @err.app.errs.should_receive(:find).and_return(@err) @@ -191,17 +191,17 @@ assigns(:app).should == @err.app assigns(:err).should == @err end - + it "should resolve the issue" do @err.should_receive(:resolve!).and_return(true) put :resolve, :app_id => @err.app.id, :id => @err.id end - + it "should display a message" do put :resolve, :app_id => @err.app.id, :id => @err.id request.flash[:success].should match(/Great news/) end - + it "should redirect to the app page" do put :resolve, :app_id => @err.app.id, :id => @err.id response.should redirect_to(app_path(@err.app)) @@ -285,6 +285,39 @@ err.issue_link.should == @issue_link.sub(/\.xml/, '') end end + + context "redmine tracker" do + let(:notice) { Factory :notice } + let(:tracker) { Factory :pivotal_tracker, :app => notice.err.app } + let(:err) { notice.err } + + before(:each) do + pending + number = 5 + @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" + body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" + stub_request(:post, "#{tracker.account}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) + + post :create_issue, :app_id => err.app.id, :id => err.id + err.reload + end + + it "should make request to Pivotal Tracker with err params" do + requested = have_requested(:post, "#{tracker.account}/issues.xml") + WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) + WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) + WebMock.should requested.with(:body => /<subject>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/subject>/) + WebMock.should requested.with(:body => /<description>.+<\/description>/m) + end + + it "should redirect to err page" do + response.should redirect_to( app_err_path(err.app, err) ) + end + + it "should create issue link for err" do + err.issue_link.should == @issue_link.sub(/\.xml/, '') + end + end end context "absent issue tracker" do diff --git a/spec/factories/issue_tracker_factories.rb b/spec/factories/issue_tracker_factories.rb index a1bb16005a..b2c20f171e 100644 --- a/spec/factories/issue_tracker_factories.rb +++ b/spec/factories/issue_tracker_factories.rb @@ -1,12 +1,19 @@ -Factory.define :lighthouseapp_tracker, :class => IssueTracker do |e| - e.issue_tracker_type 'lighthouseapp' - e.account { Factory.next :word } +Factory.define :generic_tracker, :class => IssueTracker do |e| e.api_token { Factory.next :word } e.project_id { Factory.next :word } e.association :app, :factory => :app end -Factory.define :redmine_tracker, :parent => :lighthouseapp_tracker do |e| +Factory.define :lighthouseapp_tracker, :parent => :generic_tracker do |e| + e.issue_tracker_type 'lighthouseapp' + e.account { Factory.next :word } +end + +Factory.define :redmine_tracker, :parent => :generic_tracker do |e| e.issue_tracker_type 'redmine' e.account { "http://#{Factory.next(:word)}.com" } -end \ No newline at end of file +end + +Factory.define :pivotal_tracker, :parent => :generic_tracker do |e| + e.issue_tracker_type 'pivotal' +end From fe3fde207f07572aedbe7a0b36d06372a97988d6 Mon Sep 17 00:00:00 2001 From: Ben Langfeld <ben@langfeld.me> Date: Mon, 25 Apr 2011 21:25:12 +0100 Subject: [PATCH 104/115] Make rspec play nicer --- .rspec | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.rspec b/.rspec index 53607ea52b..f518044217 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,4 @@ --colour +--tty +--drb +--format documentation From c5a7cb0e9bd8c6126afb58570d397b234c8922bf Mon Sep 17 00:00:00 2001 From: Ben Langfeld <ben@langfeld.me> Date: Mon, 25 Apr 2011 21:29:39 +0100 Subject: [PATCH 105/115] Fix a little failing spec --- spec/controllers/apps_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index dee4236f3b..568a0a16f2 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -254,7 +254,7 @@ it "should show validation notice when sufficient params are not present" do put :update, :id => @app.id, :app => { :issue_tracker_attributes => { - :issue_tracker_type => 'pivotal', :project_id => '1234', :api_token => '123123' } } + :issue_tracker_type => 'pivotal', :project_id => '1234' } } @app.reload @app.issue_tracker.should be_nil From 7798d7fd9447c20f440e9cfaf49544fe28fdeed8 Mon Sep 17 00:00:00 2001 From: Karol Hosiawa <hosiawak@gmail.com> Date: Tue, 3 May 2011 14:37:14 +0200 Subject: [PATCH 106/115] do not trigger email notifications during migration --- .../20110422152027_move_notices_to_separate_collection.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/db/migrate/20110422152027_move_notices_to_separate_collection.rb b/db/migrate/20110422152027_move_notices_to_separate_collection.rb index f3ea60ee6b..32e914c985 100644 --- a/db/migrate/20110422152027_move_notices_to_separate_collection.rb +++ b/db/migrate/20110422152027_move_notices_to_separate_collection.rb @@ -6,10 +6,14 @@ def self.up errs.each do |err| next unless err['notices'] e = Err.find(err['_id']) + # disable email notifications + old_notify = e.app.notify_on_errs? + e.app.update_attribute(:notify_on_errs, false) puts "Copying notices for Err #{err['_id']}" err['notices'].each do |notice| e.notices.create!(notice) end + e.app.update_attribute(:notify_on_errs, old_notify) mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}}) end Rake::Task["errbit:db:update_notices_count"].invoke From 217069e9504f561c275b723ec4754439272ea7de Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Tue, 3 May 2011 17:25:00 +0400 Subject: [PATCH 107/115] Reverting mongoid.example.yml change. --- config/mongoid.example.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/mongoid.example.yml b/config/mongoid.example.yml index 1b14d4df0d..67496d6938 100644 --- a/config/mongoid.example.yml +++ b/config/mongoid.example.yml @@ -26,8 +26,8 @@ test: # set these environment variables on your prod server production: <<: *defaults - host: localhost - port: 27018 - username: errbit - password: - database: errbit \ No newline at end of file + host: <%= ENV['MONGOID_HOST'] %> + port: <%= ENV['MONGOID_PORT'] %> + username: <%= ENV['MONGOID_USERNAME'] %> + password: <%= ENV['MONGOID_PASSWORD'] %> + database: <%= ENV['MONGOID_DATABASE'] %> \ No newline at end of file From ffc54363c5cb9c53ec4f053ff065e4133719f8fb Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 4 May 2011 17:15:52 +0400 Subject: [PATCH 108/115] Cut down pivotal story description since it can handle only 5000 characters. --- app/views/errs/pivotal_body.txt.erb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/views/errs/pivotal_body.txt.erb b/app/views/errs/pivotal_body.txt.erb index d820ea48f5..7a5362d516 100644 --- a/app/views/errs/pivotal_body.txt.erb +++ b/app/views/errs/pivotal_body.txt.erb @@ -12,10 +12,5 @@ See this exception on Errbit: <%= app_err_url err.app, err %> <%= pretty_hash notice.session %> Backtrace: - <%= notice.backtrace.map { |line| "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %> - - Environment: - <% notice.env_vars.each do |key, val| %> - <%= "#{key}: #{val}" %> - <% end %> + <%= notice.backtrace[0..4].map { |line| "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %> <% end %> From 453df12a48f5c15b2d5aee2fe7b3ad2174ad6e60 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 4 May 2011 21:07:43 +0400 Subject: [PATCH 109/115] Goto issue icon fixed. --- app/views/errs/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index f7fb535b15..3f975797bc 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -14,7 +14,7 @@ - if @err.issue_link.blank? %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => "#{@app.issue_tracker.issue_tracker_type}_create create-issue" - else - %span= link_to 'go to issue', @err.issue_link, :class => "#{@app.issue_tracker.issue_tracker_type}_create goto-issue" + %span= link_to 'go to issue', @err.issue_link, :class => "#{@app.issue_tracker.issue_tracker_type}_goto goto-issue" = link_to 'clear issue', clear_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Clear err issues?", :class => "clear-issue" - if @err.unresolved? %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' From 096da89f51e33c7ff588e88282aeed514b3fa972 Mon Sep 17 00:00:00 2001 From: Nick Recobra <oruen@undev.ru> Date: Wed, 4 May 2011 21:07:56 +0400 Subject: [PATCH 110/115] Pivotal tracker icons. --- public/images/pivotal_create.png | Bin 0 -> 1541 bytes public/images/pivotal_goto.png | Bin 0 -> 1561 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/images/pivotal_create.png create mode 100644 public/images/pivotal_goto.png diff --git a/public/images/pivotal_create.png b/public/images/pivotal_create.png new file mode 100644 index 0000000000000000000000000000000000000000..83e4f49ba5a318e87909496bf4737357b065f77f GIT binary patch literal 1541 zcmV+g2KxDlP)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%r%6OXRCwCNR$EM)Wf*?jQlKs6P>$gY zQK6D80}@UFB__r#nTaGM7)^FHofm3MjK&*pjCX2MHSTJcny5D{yFg@yFiR*bn-Uio z!zDlmESyhmIg}Qk=PO?aD<^fyzU0jh|Nr%U-uHRm_xV1<&d!cJjb)UlwBk;>abRGe z1dle@%diyKncYu=uoc*K*iW^!wPPpoXE=`P{{H?(JU%y>OfOokR-xf<Nk~Xg-#0fm zWqW&D0)c?6udhoe6uN=eAJ^5@-TY6$0F1}uE3?`BZgO(6q@<)sTwI(OjYeg9@%Q0y zn1CvPm6a7)TU+DbU*q$K2q1J~V2{V+z@t4WDd~l*tSkYzG7V}l7?g1lRD%<kMi%Dh z=gGov*wR{GU%zrJFhDJE>bBeM)dY=?k5`$J-e54ODBarHQpUya49a4$sOMhrxw$#+ z_W+|A;9JKv*SDx+6qL@m=!97!AVqs(VxoG#v9Y0o*rit-4oCDi85tQeJ3CvC+JA|E z-#;|tzP`R{WOu8uuuwTBFl#qGJuT_!>B`YjYhhtQ1(=<ktr%iJe!pL)rl!L9T;0&n zFnD0bsNZ+Yh}6;;)a2x(<mBW?T3VXg6GZ1u-@YxgFs`iXYRQF}K{bQp@A#Yi{Cow@ z;L)5$1mME<=9r9odwb8|M}HO-6{&i0To9?C!4xpp{mxGDT)HG7@2r?4LUmX+#gONa zhAUU(nX_lrJJ2hOi;I$*o2zQgF=H@jz@o;+M(;l50=4hx8D;0{2GXDpJRbS|{CP3Y zg~gVZCy9;>W!AJ@iC;3xjdSN@th+nH0{Vk~NVA><CIXB%Vk8rpzr?z76X{~D*eIG_ z_k{~$HRp)Qf=qWuDZ-gSOo<6%v*b(P#f!4(^QqUVsj0HGv=jxVZ)E&>3@}C|kM+>N zq>A<Cy4T(=!TS#*poc7ihsNgQ?`zl8{}i3&<z+Pq^vO;~qn5@1V-zghz?$<(nzSq~ z-nk=j2Fw0TV-x|I8!a+>GoqCIEq!QpqfY=^4m_Q>K{p%6h-#$%rdS*W8pSq9cvo1o zlI``$EivN+dba^))|=QI6bI6uQdTAp!W)PzS<ETP^4i!RQLy0Znp`cZV$=wBLma10 zMGp^VsU?+(z~^>M#-PT}%{o)ohz-W|d|R9BWTH8?f>FSDceXOU_$};+lV@96qDp78 z*_1CDeFMW61B_AfQin+oHJLLwK0Y29HArO5<;$qql!P}UR2nwI$`mNx1^f@C%H<OK z?)XhiOem$JKLT@>A+`5nsK)4YBFfXum=tGbW-5w?hK8h~qC%Xlt>WC-epF9?<g_y< z+ojDnv9H!6BO~f~EH5vQp4SA1`u%v2Ry%KDp$=9?O8KLJbaZqG&QvKaEfwnzsYf<f z;LT;llH2W;rluy<A7qajj^(eas(KF3>BqbJZb?bWbw<j~tE;O@I-8rDc`1lQ83-L| zWUCwMRZ~-=1cQE<F*TvMxcC<hdeC|71hsttO{G*)La9Kky*?!LN^;rM)Axa2_K1)= z$#kYP>Nb2VIGxT0Y#BZ5mU$N&To{AE%*>1eWVCcd+DT(iPtU&QqVuk<E*TviRZ2zG z)1d5ZD3-shtn7=upod+3OaS?QaB%QdG=+^#MqpB>&uEsw{#ismS3g}>HPoX>BPg#v z2*O`kS@|=--=Yqk<g#bSd)M&ra0{qP<4lusiVCfupy1K`VpP0dZ}i^9tl1wZ)grq3 zD%9*f_T9g^-kM;a;DmV_<B{p}`79I!8tK&68)nCGp)S*Key;;?4n6S=KD!>z^u+gS ztkbZMQ11pbjvW-*0A|Eo^7>2Kw-7`aBj|_dtf3+M@R~LK>wjJ7_pDslE3hUWx3I$9 r_q<Uo!`!f6_m0ey0)C?F9{~mcRP!}Pe@V_s00000NkvXXu0mjf9`N^W literal 0 HcmV?d00001 diff --git a/public/images/pivotal_goto.png b/public/images/pivotal_goto.png new file mode 100644 index 0000000000000000000000000000000000000000..b338690aaed87315b8385f54b8c4d4b4866bc74a GIT binary patch literal 1561 zcmV+!2Il#RP)<h;3K|Lk000e1NJLTq0015U0015c1^@s6J20-I0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%yGcYrRCwCVS6fV6RTSMb!)q9Z$M6gg zD2l>Z+E7R+5NL_P*d}dkn;J|%`eCXyq9$rIrhb|DS4mJ~znWm8AL<XJABZU-sgRjI zBcTDQZPJ2(FuV%`48shrb!N`Z*n!%bCiWydcewZ5z4l&v?{zMw&CN~uKbGmK2flLU zN)awiu$N#du=4Pue%K=HBJ9V#d-vXbBEXj~U#`RDbBo2YKQS>;X!tGh@$u@p&*zhk zjSX2@S&`M%RS5(FmvH;z>gwvN{|Oj?ak#X^#>T#roSZBvDJc>Y6C-A`S(#pZKNt)W zPzA8KxF}0YOZ@#M9)E}c0*?&rcDo(8v?nDc?aRu_5`ZhypeB<^85cn{IDu(oVP<BA zEOcYb@tT^N#eW3`s1;6o?RI-5LF3}$RHmfY@AoT8*VosTaq&5WvRbX`8VNo<J<a`Y zU^D=HJrXBo3l_dc9Su-A<DwI0iGURC2?+`6{@U7_3L;FeI2;b+os5hO@pwEnsQnlC z^Sx~|cDY=Y$nJVUL4k5iVAgJ8VnWi>)0Lx#t=ZXG6<~IDwql3@dA(j4A0H3mab<07 zZU2@Tqki8oBT`FaP-A0bl9Q7oX=!O{j~|^oapQ(~U|d<1m68jK1=S3W@9~}d{Cow@ z;L)6B1kj4@4N)2Q_4SqGrQZq*3st>1E{IgnU<#P)PFI(>PoI{+s7EXkqB<x(G37a= z_WXG%K5#(Y1HCdgHz&EdxvJJ2GX{eOEUc@m8&yf_2te&ydPdo~x`8z4pKiDOdgzeE zP6x%7mM0003}uOFxe_;TmP-c@%H5ux5DVxJ`XSAF5|{`uP8jS)%;Sr!D>soY){2dy z>FqsvQW9fx#9~FJ;ZX{4W)e$6yx6Sy;yQIoe9Oz~c4}&>%+Jpo!1RrbUyB09sN}I8 z8kkhE-dwlZ+r@w9uMp@qi{OE~({lU51@$*YXJKJMO#*$g)6uBUL;+(ItlYqw^Gceu zEY97$DKRGNKQoO|1Y~Zuisx!bDfupaXmz7c09&Jg^#<K+93!fc`c1KT7^s15l3-X^ zw30>o<d!I4(7Qn}y@}01ao|cRDUm>MjghIpU!A>enDc|Tb<_xUO&q&+8HWe6)RIcy z@VOBM3~IdGtTSbe*kD{QG&RX)CYod2Z)A7v+BLPe?9=z<+1F2~XF*4dJa_b{p>#Hz zP5GkHH!zl?fH6v=)M3&?P38>VyLT@%YLLjPbLUXAaS8fDR1ORbNJmG9I*lsMekM7u z?3aqxR<Vc2Z)9XdDINV0n6nJ2y%nWg7@aOextm##;>^rUMbY5kpzPeaQ=G?-i*wUp zN{tGwt*uQO8yjWcci)OBd|VC>537JmOG}OOn!r%MAGXqJ=XEU9A(4?%{wN@wot;uu zRVBs6#d7A%8KdDIT;=8Ea_rbKrR%-Dy;5IaujG>KQNyvk6%`fF<2vzRSN~pARCJM% za`XNB_my-uG&E374JqU~yOl9WCeZ`+VxSZZ`eDY@gdIC}{H#H@I**;8)&<a1N+l(f z3dDKTDyG+wBS+Mg%rrMQ3$hUqrb?tUr7>9em~}dxwb=g0mUGOkv7r@Xurf6@r4D^Y zOEF+ZY_{(1ZllI!Wo6RR(xN~oCntpm3{_8qva_LB-jb4%vyq_Nefn4c@_m1Q|6w$R zO=m#rXp}xO(S?)hK=?FThCZ`IY)l$KNk4+{?%A{FC%p3pb?8w(_UyRdIy5wN6jY^g zrb#(Ph3dr2A`gFledzBl>NEQTrJ6%mcVPd!*tYUGpSKp+C!q9Aj7KK2vQiLeq*H(1 zFguP5`+$b?dli7w7@e>1xb?wIALoO9H|!(SyB4Ej2Zc6(nK76A{AK5_BZweI&<oL7 zLK$4R&6*B8sefT|VK2k#dECMZ!uR<_u>k9Z{SrAcPv!qke+w`Gn*T`p=CiU~00000 LNkvXXu0mjfkhSiQ literal 0 HcmV?d00001 From 7a2aaa70c9b8f0c5b5231b794ff11616b91c20c6 Mon Sep 17 00:00:00 2001 From: Stefano Verna <stefano.verna@welaika.com> Date: Tue, 28 Jun 2011 14:01:05 +0200 Subject: [PATCH 111/115] Logging --- app/mailers/mailer.rb | 10 +++++----- app/models/notice.rb | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 1cf92dfce3..0fc934d6a2 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -1,24 +1,24 @@ class Mailer < ActionMailer::Base default :from => Errbit::Config.email_from - + def err_notification(notice) @notice = notice @app = notice.err.app - + mail({ :to => @app.watchers.map(&:address), :subject => "[#{@app.name}][#{@notice.err.environment}] #{@notice.err.message}" }) end - + def deploy_notification(deploy) @deploy = deploy @app = deploy.app - + mail({ :to => @app.watchers.map(&:address), :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}" }) end - + end diff --git a/app/models/notice.rb b/app/models/notice.rb index 8c3fc9c0ed..bd6177d34f 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -55,7 +55,7 @@ def user_agent agent_string = env_vars['HTTP_USER_AGENT'] agent_string.blank? ? nil : UserAgent.parse(agent_string) end - + def request read_attribute(:request) || {} end @@ -73,6 +73,7 @@ def session end def deliver_notification + puts "Deliver notification!" Mailer.err_notification(self).deliver end @@ -83,6 +84,7 @@ def cache_last_notice_at protected def should_notify? + puts "Should notify? #{err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any?}" err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? end From b8c472a736e861859e865525a4954060a47046f3 Mon Sep 17 00:00:00 2001 From: Stefano Verna <stefano.verna@welaika.com> Date: Tue, 28 Jun 2011 14:05:02 +0200 Subject: [PATCH 112/115] More logging --- app/models/notice.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/notice.rb b/app/models/notice.rb index bd6177d34f..4b2b56dda7 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -84,7 +84,10 @@ def cache_last_notice_at protected def should_notify? - puts "Should notify? #{err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any?}" + puts "notify_on_errs: #{err.app.notify_on_errs?}" + puts "email_at_notices: #{Errbit::Config.email_at_notices.to_json}" + puts "notices_count: #{err.notices.count}" + puts "watchers: #{err.app.watchers.count}" err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? end From 256637c343e299108879971254690acf59b003ef Mon Sep 17 00:00:00 2001 From: Stefano Verna <stefano.verna@welaika.com> Date: Tue, 28 Jun 2011 14:06:41 +0200 Subject: [PATCH 113/115] Adding host for mails --- config/environments/production.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/environments/production.rb b/config/environments/production.rb index 6c1ea4ffb5..6a540aff6d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -46,4 +46,5 @@ # Send deprecation notices to registered listeners config.active_support.deprecation = :notify + config.action_mailer.default_url_options = { :host => 'errbit.welaika.com' } end From 52c1c28603f57d2e5f562e729ca45d621d336378 Mon Sep 17 00:00:00 2001 From: Stefano Verna <stefano.verna@welaika.com> Date: Fri, 2 Sep 2011 09:46:47 +0200 Subject: [PATCH 114/115] Added log --- app/mailers/mailer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 0fc934d6a2..db3e1cbb31 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -5,6 +5,7 @@ def err_notification(notice) @notice = notice @app = notice.err.app + puts "To: #{@app.watchers.map(&:address).join(", ")}" mail({ :to => @app.watchers.map(&:address), :subject => "[#{@app.name}][#{@notice.err.environment}] #{@notice.err.message}" From 37116fdef25442856749aaf12b450fd6412c85c3 Mon Sep 17 00:00:00 2001 From: Stefano Verna <stefano.verna@welaika.com> Date: Tue, 20 Sep 2011 10:10:47 +0200 Subject: [PATCH 115/115] v2.1 accepted --- lib/hoptoad.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hoptoad.rb b/lib/hoptoad.rb index 4bf64e65ab..1db916c905 100644 --- a/lib/hoptoad.rb +++ b/lib/hoptoad.rb @@ -4,13 +4,13 @@ module V2 class ApiVersionError < StandardError def initialize - super "Wrong API Version: Expecting v2.0" + super "Wrong API Version: Expecting v2.0 or v2.1" end end def self.parse_xml(xml) parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice'] - raise ApiVersionError unless parsed && parsed['version'] == '2.0' + raise ApiVersionError unless parsed && (parsed['version'] == '2.0' || parsed['version'] == '2.1') rekeyed = rekey(parsed) rekeyed['fingerprint'] = Digest::MD5.hexdigest(rekeyed['error']['backtrace'].to_s) rekeyed