diff --git a/.gitignore b/.gitignore index 95666115a0..f976d0d4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ log/*.log tmp/**/* config/config.yml config/deploy.rb -config/mongoid.yml \ No newline at end of file +config/mongoid.yml +.rvmrc +*~ +*.rbc diff --git a/.rspec b/.rspec index 53607ea52b..f518044217 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,4 @@ --colour +--tty +--drb +--format documentation diff --git a/Gemfile b/Gemfile index 61e1d07e76..044a001d00 100644 --- a/Gemfile +++ b/Gemfile @@ -1,19 +1,28 @@ source 'http://rubygems.org' -gem 'rails', '3.0.0.rc' -gem 'libxml-ruby' -gem 'bson_ext', :require => nil -gem 'mongoid', '2.0.0.beta.15' +gem 'rails', '3.0.5' +gem 'nokogiri' +gem 'mongoid', '2.0.0.rc.8' gem 'haml' gem 'will_paginate' -gem 'devise', '1.1.1' +gem 'devise', '~> 1.1.8' +gem 'lighthouse-api' +gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" +gem 'mongoid_rails_migrations' +gem 'useragent', '~> 0.3.1' +gem 'pivotal-tracker' + +platform :ruby do + gem 'bson_ext', '~> 1.2' +end group :development, :test do - gem 'rspec-rails', '>= 2.0.0.beta.19' + gem 'rspec-rails', '~> 2.5' + gem 'webmock', :require => false end group :test do - gem 'rspec', '>= 2.0.0.beta.19' - gem 'database_cleaner', '0.5.2' + gem 'rspec', '~> 2.5' + gem 'database_cleaner', '~> 0.6.0' gem 'factory_girl_rails' end diff --git a/Gemfile.lock b/Gemfile.lock index 68a23b873b..b99c0c6437 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,122 +1,155 @@ +GIT + remote: git://github.com/oruen/redmine_client.git + revision: 0df20a8b695869b03cfa129560b938a0af346add + specs: + redmine_client (0.0.1) + activeresource (>= 2.3.0) + 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.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.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.7) + tzinfo (~> 0.3.23) + activemodel (3.0.5) + activesupport (= 3.0.5) 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) - bcrypt-ruby (2.1.2) - bson (1.0.4) - bson_ext (1.0.4) + i18n (~> 0.4) + activerecord (3.0.5) + activemodel (= 3.0.5) + activesupport (= 3.0.5) + arel (~> 2.0.2) + tzinfo (~> 0.3.23) + activeresource (3.0.5) + activemodel (= 3.0.5) + activesupport (= 3.0.5) + activesupport (3.0.5) + addressable (2.2.5) + arel (2.0.9) + bcrypt-ruby (2.1.4) + bson (1.3.0) + bson_ext (1.3.0) builder (2.1.2) - database_cleaner (0.5.2) - devise (1.1.1) + crack (0.1.8) + database_cleaner (0.6.7) + devise (1.1.9) bcrypt-ruby (~> 2.1.2) - warden (~> 0.10.7) + warden (~> 1.0.2) 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.3) + factory_girl_rails (1.0.1) factory_girl (~> 1.3) - rails (>= 3.0.0.beta4) - haml (3.0.16) - i18n (0.4.1) + 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) - mail (2.2.5) + lighthouse-api (2.0) + activeresource (>= 3.0.0) + activesupport (>= 3.0.0) + mail (2.2.17) activesupport (>= 2.3.6) - mime-types - treetop (>= 1.4.5) + i18n (>= 0.4.0) + 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.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) - nokogiri (1.4.3.1) + 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) + 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.1) - rack-mount (0.6.9) + rack (1.2.2) + rack-mount (0.6.14) rack (>= 1.0.0) - rack-test (0.5.4) + rack-test (0.5.7) 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.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.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) - 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) + rest-client (1.5.1) + mime-types (>= 1.16) + 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.22) - warden (0.10.7) + tzinfo (0.3.26) + useragent (0.3.1) + warden (1.0.3) rack (>= 1.0.0) - webrat (0.7.2.beta.1) - nokogiri (>= 1.2.0) - rack (>= 1.0) - rack-test (>= 0.5.3) + webmock (1.6.2) + addressable (>= 2.2.2) + crack (>= 0.1.7) will_paginate (3.0.pre2) PLATFORMS ruby DEPENDENCIES - bson_ext - database_cleaner (= 0.5.2) - devise (= 1.1.1) + bson_ext (~> 1.2) + database_cleaner (~> 0.6.0) + devise (~> 1.1.8) 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) + lighthouse-api + mongoid (= 2.0.0.rc.8) + mongoid_rails_migrations + nokogiri + pivotal-tracker + rails (= 3.0.5) + redmine_client! + rspec (~> 2.5) + rspec-rails (~> 2.5) + useragent (~> 0.3.1) + webmock will_paginate diff --git a/README.md b/README.md index 3a5c213ead..6817566d12 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,90 +22,116 @@ 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 --pre - + + 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! +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 +* 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** +* 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 ---- -* Add a deployment view * Add ability for watchers to be configured for types of notifications they should receive 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). 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 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/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index e357d5ad45..f3a31cbab0 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -1,28 +1,38 @@ 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 - @errs = @app.errs.paginate + respond_to do |format| + format.html do + @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 + @errs = @app.errs.unresolved.ordered + 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) @@ -30,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) @@ -39,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/deploys_controller.rb b/app/controllers/deploys_controller.rb index f026c8901e..aa75a879a4 100644 --- a/app/controllers/deploys_controller.rb +++ b/app/controllers/deploys_controller.rb @@ -1,16 +1,39 @@ class DeploysController < ApplicationController - + + protect_from_forgery :except => :create + skip_before_filter :verify_authenticity_token, :only => :create skip_before_filter :authenticate_user!, :only => :create - + 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], + :message => params[:deploy][:message] + } + 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 - + + def index + # 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 + end \ No newline at end of file diff --git a/app/controllers/errs_controller.rb b/app/controllers/errs_controller.rb index 12856ac9ed..3b18616a69 100644 --- a/app/controllers/errs_controller.rb +++ b/app/controllers/errs_controller.rb @@ -1,45 +1,83 @@ 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 - @errs = Err.for_apps(app_scope).unresolved.ordered.paginate(:page => params[:page]) + respond_to do |format| + format.html do + @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 + 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]) + @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) end - + def show - @err = @app.errs.find(params[:id]) - 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 end - + + def create_issue + set_tracker_params + + 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 => 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 + + def clear_issue + @err.update_attribute :issue_link, nil + 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 + # 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 errs_path + + 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) end - + + 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 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/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 70aaa70634..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]) + @users = User.paginate(:page => params[:page], :per_page => current_user.per_page) end def show @@ -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/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be7945c..a831f1f0c9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +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 + + def total_from_tallies(tallies) + tallies.values.inject(0) {|sum, n| sum + n} + end + private :total_from_tallies + + 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/helpers/errs_helper.rb b/app/helpers/errs_helper.rb new file mode 100644 index 0000000000..a84e12d182 --- /dev/null +++ b/app/helpers/errs_helper.rb @@ -0,0 +1,10 @@ +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/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 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/mailers/mailer.rb b/app/mailers/mailer.rb index 06f65eee7d..db3e1cbb31 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -1,24 +1,25 @@ class Mailer < ActionMailer::Base default :from => Errbit::Config.email_from - + 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.message}" + :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 \ No newline at end of file + +end diff --git a/app/models/app.rb b/app/models/app.rb index 42c317ad18..4771706f2c 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -1,43 +1,76 @@ class App include Mongoid::Document include Mongoid::Timestamps - + field :name, :type => String field :api_key field :resolve_errs_on_deploy, :type => Boolean, :default => false - key :name - + field :notify_on_errs, :type => Boolean, :default => true + field :notify_on_deploys, :type => Boolean, :default => true + + # 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_create do |r| + r.id = ActiveSupport::SecureRandom.hex + end + embeds_many :watchers 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 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 - + + # 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 - + def generate_api_key self.api_key ||= ActiveSupport::SecureRandom.hex end - + + def check_issue_tracker + if issue_tracker.present? + issue_tracker.valid? + issue_tracker.errors.full_messages.each do |error| + errors[:base] << error + end if issue_tracker.errors + end + end end diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 9158db5f98..3e9a79010a 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -6,6 +6,9 @@ class Deploy field :repository field :environment field :revision + field :message + + index :created_at, Mongo::DESCENDING embedded_in :app, :inverse_of => :deploys @@ -25,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 496c773cd6..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 @@ -9,39 +9,45 @@ class Err field :fingerprint 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.message || klass + super || klass end - -end \ No newline at end of file + +end diff --git a/app/models/issue_tracker.rb b/app/models/issue_tracker.rb new file mode 100644 index 0000000000..7a6dc235eb --- /dev/null +++ b/app/models/issue_tracker.rb @@ -0,0 +1,102 @@ +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_params + + embedded_in :app, :inverse_of => :issue_tracker + + field :account, :type => String + field :api_token, :type => String + field :project_id, :type => String + field :issue_tracker_type, :type => String, :default => 'lighthouseapp' + + def create_issue err + 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 + def create_redmine_issue err + token = api_token + acc = account + RedmineClient::Base.configure do + self.token = token + self.site = acc + end + 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\?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 + + # updating lighthouse account + Lighthouse::Ticket.site + + ticket = Lighthouse::Ticket.new(:project_id => project_id) + ticket.title = issue_title err + + ticket.body = self.class.lighthouseapp_body_template.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 + + def issue_title err + "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" + end + + def check_params + 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 = case issue_tracker_type + when 'lighthouseapp' + "You must specify your Lighthouseapp account, api token and project id" + 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 + 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")) + 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/models/notice.rb b/app/models/notice.rb index 95568f0a3c..4b2b56dda7 100644 --- a/app/models/notice.rb +++ b/app/models/notice.rb @@ -1,31 +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'], @@ -35,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'], @@ -44,35 +50,77 @@ def self.from_xml(hoptoad_xml) :notifier => hoptoad_notice['notifier'] }) 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 - + def env_vars request['cgi-data'] || {} end - + def params request['params'] || {} end - + def session request['session'] || {} end - + def deliver_notification + puts "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? - Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? + + def should_notify? + 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 + + + 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 + + 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 \ No newline at end of file + end +end + diff --git a/app/models/user.rb b/app/models/user.rb index 603d1d1a29..61146f6c22 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,5 @@ class User + PER_PAGE = 30 include Mongoid::Document include Mongoid::Timestamps @@ -8,8 +9,10 @@ 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 validates_presence_of :name @@ -20,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/app/views/apps/_configuration_instructions.html.haml b/app/views/apps/_configuration_instructions.html.haml index b210cc6127..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} # Note: Deployment notifications only work on port 80 + 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 + diff --git a/app/views/apps/_fields.html.haml b/app/views/apps/_fields.html.haml index 0a2c82031e..43a2267382 100644 --- a/app/views/apps/_fields.html.haml +++ b/app/views/apps/_fields.html.haml @@ -3,14 +3,22 @@ %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' + %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| + = f.fields_for :watchers do |w| %div.watcher.nested %div.choose = w.radio_button :watcher_type, :user @@ -20,4 +28,35 @@ %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 + +%fieldset + %legend Issue tracker + = f.fields_for :issue_tracker do |w| + %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') + = w.radio_button :issue_tracker_type, :redmine + = label_tag :issue_tracker_type_redmine, 'Redmine', :for => label_for_attr(w, 'issue_tracker_type_redmine') + = 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.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/apps/index.html.haml b/app/views/apps/index.html.haml index 3916553af4..4519184029 100644 --- a/app/views/apps/index.html.haml +++ b/app/views/apps/index.html.haml @@ -12,9 +12,9 @@ - @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? + - if app.errs.count > 0 = link_to app.errs.unresolved.count, app_errs_path(app) - else \- @@ -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 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 57c6a813ad..200ff75cb1 100644 --- a/app/views/apps/show.html.haml +++ b/app/views/apps/show.html.haml @@ -1,7 +1,11 @@ - 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 + %strong Deploy Count: + = @app.deploys.count %strong API Key: = @app.api_key - content_for :action_bar do @@ -23,9 +27,32 @@ %td %em Sadly, no one is watching this app -- if @app.errs.any? - %h3 Errs +%h3 Latest Deploys +- if @deploys.any? + %table.deploys + %thead + %tr + %th When + %th Who + %th Message + %th Repository + %th Revision + + %tbody + - @deploys.each do |deploy| + %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' +- else + %h3 No deploys + +- if @app.errs.count > 0 + %h3.clear 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 + %h3.clear No errs have been caught yet, make sure you setup your app + = render 'configuration_instructions', :app => @app 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..8d99fa4dfd --- /dev/null +++ b/app/views/deploys/index.html.haml @@ -0,0 +1,3 @@ +- content_for :title, 'Deploys' += render 'table', :deploys => @deploys += will_paginate @deploys, :previous_label => '« Previous', :next_label => 'Next »' 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/_table.html.haml b/app/views/errs/_table.html.haml index 649175a806..96c269e1e5 100644 --- a/app/views/errs/_table.html.haml +++ b/app/views/errs/_table.html.haml @@ -6,20 +6,22 @@ %th Latest %th Deploy %th Count + %th Resolve %tbody - 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) %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) + %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 %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/_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/errs/index.atom.builder b/app/views/errs/index.atom.builder new file mode 100644 index 0000000000..b0a77d5d5c --- /dev/null +++ b/app/views/errs/index.atom.builder @@ -0,0 +1,4 @@ +atom_feed do |feed| + feed.title("Errbit notices at #{root_url}") + render :partial => "errs/list", :locals => {:feed => feed} +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 diff --git a/app/views/errs/lighthouseapp_body.txt.erb b/app/views/errs/lighthouseapp_body.txt.erb new file mode 100644 index 0000000000..709d194f5f --- /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 ## + <%= pretty_hash(notice.params) %> + + ## Session ## + <%= pretty_hash(notice.session) %> + + ## Backtrace ## + + <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** + <% end %> + + + ## Environment ## + <% for key, val in notice.env_vars %> + <%= key %>: <%= val %> + <% end %> +<% end %> diff --git a/app/views/errs/pivotal_body.txt.erb b/app/views/errs/pivotal_body.txt.erb new file mode 100644 index 0000000000..7a5362d516 --- /dev/null +++ b/app/views/errs/pivotal_body.txt.erb @@ -0,0 +1,16 @@ +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[0..4].map { |line| "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %> +<% end %> diff --git a/app/views/errs/redmine_body.txt.erb b/app/views/errs/redmine_body.txt.erb new file mode 100644 index 0000000000..2a7e975ec4 --- /dev/null +++ b/app/views/errs/redmine_body.txt.erb @@ -0,0 +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 + +
<%= pretty_hash(notice.params) %>
+ +h2. Session + +
<%= pretty_hash(notice.session) %>
+ +h2. Backtrace + +
+<% for line in notice.backtrace %><%= line['number'] %>:  <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> *<%= line['method'] %>*
+<% end %>
+
+ +h2. Environment + +
+<% for key, val in notice.env_vars %>
+<%= key %>: <%= val %>
+<% end %>
+
+<% end %> diff --git a/app/views/errs/show.html.haml b/app/views/errs/show.html.haml index 99b47cc5f0..3f975797bc 100644 --- a/app/views/errs/show.html.haml +++ b/app/views/errs/show.html.haml @@ -1,17 +1,26 @@ - 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: = @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? + - 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 => "#{@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}_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' + +%h4= @notice.try(:message) -%h4= @notice.message - = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination' viewing occurrence #{@notices.current_page} of #{@notices.total_pages} @@ -23,22 +32,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 - -#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 +- if @notice + #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 new file mode 100644 index 0000000000..b11e19859d --- /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 + diff --git a/app/views/notices/_environment.html.haml b/app/views/notices/_environment.html.haml index ebf533ef8b..a68535f0e2 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] diff --git a/app/views/notices/_summary.html.haml b/app/views/notices/_summary.html.haml index 06889e6912..904b48650f 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) 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/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 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/config/deploy.example.rb b/config/deploy.example.rb index ae5e2b7289..5a3203e4e1 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" @@ -29,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 @@ -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') 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/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 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/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/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 diff --git a/config/routes.rb b/config/routes.rb index 9f4cd9aacd..6d9ad1e813 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,18 +14,22 @@ get :all end end - + resources :apps do resources :errs do resources :notices member do put :resolve + post :create_issue + delete :clear_issue end end + + resources :deploys, :only => [:index] end - + devise_for :users - + root :to => 'apps#index' - + end 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..32e914c985 --- /dev/null +++ b/db/migrate/20110422152027_move_notices_to_separate_collection.rb @@ -0,0 +1,26 @@ +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']) + # 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 + Rake::Task["errbit:db:update_err_message"].invoke + end + + def self.down + end + +end diff --git a/db/seeds.rb b/db/seeds.rb index 880432a26a..87fca02850 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,13 +10,12 @@ puts "-- password: #{admin_pass}" puts "" puts "Be sure to change these credentials ASAP!" -u = User.create!({ +user = User.where(:email => admin_email).first || User.new({ :name => 'Errbit Admin', :email => admin_email, :password => admin_pass, :password_confirmation => admin_pass }) -#Admin is protected -u.admin=true -u.save \ No newline at end of file +user.admin = true +user.save! diff --git a/lib/hoptoad.rb b/lib/hoptoad.rb index ee6d799022..1db916c905 100644 --- a/lib/hoptoad.rb +++ b/lib/hoptoad.rb @@ -1,23 +1,23 @@ module Hoptoad module V2 require 'digest/md5' - + 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 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/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/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/public/images/lighthouseapp_create.png b/public/images/lighthouseapp_create.png new file mode 100644 index 0000000000..a28b9a0c33 Binary files /dev/null and b/public/images/lighthouseapp_create.png differ diff --git a/public/images/lighthousehouseapp_goto.png b/public/images/lighthousehouseapp_goto.png new file mode 100644 index 0000000000..d7e468b94b Binary files /dev/null and b/public/images/lighthousehouseapp_goto.png differ diff --git a/public/images/pivotal_create.png b/public/images/pivotal_create.png new file mode 100644 index 0000000000..83e4f49ba5 Binary files /dev/null and b/public/images/pivotal_create.png differ diff --git a/public/images/pivotal_goto.png b/public/images/pivotal_goto.png new file mode 100644 index 0000000000..b338690aae Binary files /dev/null and b/public/images/pivotal_goto.png differ diff --git a/public/images/redmine_create.png b/public/images/redmine_create.png new file mode 100644 index 0000000000..a7f8c63a68 Binary files /dev/null and b/public/images/redmine_create.png differ diff --git a/public/images/redmine_goto.png b/public/images/redmine_goto.png new file mode 100644 index 0000000000..39bd0ae2c5 Binary files /dev/null and b/public/images/redmine_goto.png differ diff --git a/public/images/thumbs-up.png b/public/images/thumbs-up.png new file mode 100644 index 0000000000..03dad8404c Binary files /dev/null and b/public/images/thumbs-up.png differ diff --git a/public/javascripts/form.js b/public/javascripts/form.js index a7b6aeb89e..b0f6c64dea 100644 --- a/public/javascripts/form.js +++ b/public/javascripts/form.js @@ -1,16 +1,19 @@ $(function(){ activateNestedForms(); - + if($('div.watcher.nested').length) activateWatcherTypeSelector(); + + if($('div.issue_tracker.nested').length) + activateIssueTrackerTypeSelector(); }); function activateNestedForms() { $('.nested-wrapper').each(function(){ var wrapper = $(this); - + makeNestedItemsDestroyable(wrapper); - + var addLink = $('').text('add another').addClass('add-nested'); addLink.click(appendNestedItem); wrapper.append(addLink); @@ -32,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')); @@ -67,4 +70,13 @@ function activateWatcherTypeSelector() { wrapper.find('div.choosen').removeClass('choosen'); wrapper.find('div.'+choosen).addClass('choosen'); }); +} + +function activateIssueTrackerTypeSelector() { + $('div.issue_tracker input[name*=issue_tracker_type]').live('click', function(){ + var chosen = $(this).val(); + var wrapper = $(this).closest('.nested'); + wrapper.find('div.chosen').removeClass('chosen'); + wrapper.find('div.'+chosen).addClass('chosen'); + }); } \ No newline at end of file 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 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 diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 03ef5db6fd..08f6ee4cb7 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,8 +34,8 @@ 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 { - width: 900px; +#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,8 +194,14 @@ a.action { float: right; font-size: 0.9em;} #content { padding: 20px; border-top: 1px solid #C6C6C6; background-color: #FFF; -} - +} + +#content a.button { + float: right; + display: block; + margin-bottom: 10px; +} + /* Footer */ #footer { padding: 20px 0; @@ -205,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; } @@ -238,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; } @@ -275,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; @@ -294,6 +300,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; @@ -343,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 { @@ -356,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; } @@ -434,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; } @@ -469,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; } @@ -493,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, div.issue_tracker.nested .pivotal { display: none; } -div.nested.watcher .choosen { +div.nested.watcher .choosen, div.nested.issue_tracker .chosen { display: block; } -div.nested.watcher .choose { + +div.nested.watcher .choose, div.nested.issue_tracker .choose { margin-bottom: 0.5em; } @@ -550,10 +559,10 @@ 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; + word-wrap: break-word; } table.errs td.message em { color: #727272; @@ -565,11 +574,56 @@ 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; } +#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.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; +} + +#action-bar a.redmine_goto { + 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; @@ -605,4 +659,4 @@ table.backtrace li { table.backtrace li.in-app { color: #2adb2e; background-color: #2f2f2f; -} \ No newline at end of file +} diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 2d2d45d17d..568a0a16f2 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -1,10 +1,11 @@ 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} - + describe "GET /apps" do context 'when logged in as an admin' do it 'finds all apps' do @@ -15,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)) @@ -30,37 +31,71 @@ end end end - + describe "GET /apps/:id" do context 'logged in as an admin' do + before(:each) do + @user = Factory(:admin) + sign_in @user + @app = Factory(:app) + @err = Factory :err, :app => @app + @notice = Factory :notice, :err => @err + end + it 'finds the app' do - sign_in Factory(:admin) - app = Factory(:app) - get :show, :id => app.id - assigns(:app).should == app + 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 + + 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 + + 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 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 @@ -69,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) @@ -77,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) @@ -108,64 +143,153 @@ end end end - + 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 - 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)) end - + it "should display a message" do put :update, :id => @app.id, :app => {} 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 - @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_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_tracker.should be_nil + end + end + + context "lighthouseapp" do + it "should save tracker params" do + 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_tracker + 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_tracker_attributes => { + :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123' + } } + @app.reload + + @app.issue_tracker.should be_nil + 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 => 'redmine', :project_id => '1234', :api_token => '123123' + } } + @app.reload + + @app.issue_tracker.should be_nil + 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' } } + @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/deploys_controller_spec.rb b/spec/controllers/deploys_controller_spec.rb index fe77f6f7ba..72519c4d9d 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 @@ -8,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 @@ -25,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 @@ -38,5 +42,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 diff --git a/spec/controllers/errs_controller_spec.rb b/spec/controllers/errs_controller_spec.rb index f0dc622703..283a0671c6 100644 --- a/spec/controllers/errs_controller_spec.rb +++ b/spec/controllers/errs_controller_spec.rb @@ -1,29 +1,61 @@ 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 - it "gets a paginated list of unresolved errs" do - sign_in Factory(:admin) - errs = WillPaginate::Collection.new(1,30) - 3.times { errs << Factory(:err) } - Err.should_receive(:unresolved).and_return( - mock('proxy', :ordered => mock('proxy', :paginate => errs)) - ) + before(:each) do + @user = Factory(:admin) + sign_in @user + @notice = Factory :notice + @err = @notice.err + end + + 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 + pending "Turning off long running spec" + 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 it 'gets a paginated list of unresolved errs for the users apps' do sign_in(user = Factory(:user)) @@ -36,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 @@ -51,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)) @@ -64,37 +96,62 @@ 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 "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 + + 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 before do sign_in(@user = Factory(:user)) @@ -103,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 @@ -116,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) @@ -134,21 +191,206 @@ 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 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 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 + + 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}/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}/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 + + 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 + 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 + + 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 diff --git a/spec/controllers/notices_controller_spec.rb b/spec/controllers/notices_controller_spec.rb index 8e632dcec4..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,19 +11,27 @@ 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) email.subject.should include(@notice.err.message) + email.subject.should include("[#{@app.name}]") + email.subject.should include("[#{@notice.err.environment}]") end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index f05a6b3ecc..68da52f7b9 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 @@ -48,6 +54,16 @@ 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 + + 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 @@ -61,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 @@ -100,19 +117,29 @@ 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 + + it "should has auth token" do + post :create, @attrs + User.last.authentication_token.should_not be_blank end end @@ -145,6 +172,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 diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 0000000000..f6a3dd4dc8 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +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 ea72f41135..ed56a9898b 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' -end \ No newline at end of file + d.revision ActiveSupport::SecureRandom.hex(10) +end diff --git a/spec/factories/issue_tracker_factories.rb b/spec/factories/issue_tracker_factories.rb new file mode 100644 index 0000000000..b2c20f171e --- /dev/null +++ b/spec/factories/issue_tracker_factories.rb @@ -0,0 +1,19 @@ +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 :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 + +Factory.define :pivotal_tracker, :parent => :generic_tracker do |e| + e.issue_tracker_type 'pivotal' +end 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 } 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<notice version="2.0"> + <api-key>APIKEY</api-key> + <notifier> + <name>Hoptoad Notifier</name> + <version>2.3.2</version> + <url>http://hoptoadapp.com</url> + </notifier> + <error> + <class>HoptoadTestingException</class> + <message>HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.</message> + <backtrace> + <line number="425" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run__2115867319__process_action__262109504__callbacks"/> + <line number="404" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="send"/> + <line number="404" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run_process_action_callbacks"/> + <line number="93" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="send"/> + <line number="93" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="run_callbacks"/> + <line number="17" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/abstract_controller/callbacks.rb" method="process_action"/> + <line number="30" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/instrumentation.rb" method="process_action"/> + <line number="52" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/notifications.rb" method="instrument"/> + <line number="21" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/notifications/instrumenter.rb" method="instrument"/> + <line number="52" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/notifications.rb" method="instrument"/> + <line number="29" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/instrumentation.rb" method="process_action"/> + <line number="17" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/rescue.rb" method="process_action"/> + <line number="105" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/abstract_controller/base.rb" method="process"/> + <line number="40" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/abstract_controller/rendering.rb" method="process"/> + <line number="133" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal.rb" method="dispatch"/> + <line number="14" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal/rack_delegation.rb" method="dispatch"/> + <line number="173" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_controller/metal.rb" method="action"/> + <line number="62" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="call"/> + <line number="62" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="dispatch"/> + <line number="27" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="call"/> + <line number="148" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/route_set.rb" method="call"/> + <line number="89" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/code_generation.rb" method="recognize"/> + <line number="66" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/code_generation.rb" method="optimized_each"/> + <line number="88" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/code_generation.rb" method="recognize"/> + <line number="139" file="[GEM_ROOT]/gems/rack-mount-0.6.9/lib/rack/mount/route_set.rb" method="call"/> + <line number="489" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/routing/route_set.rb" method="call"/> + <line number="41" file="[GEM_ROOT]/gems/haml-3.0.15/lib/sass/plugin/rack.rb" method="call"/> + <line number="14" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/head.rb" method="call"/> + <line number="24" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/methodoverride.rb" method="call"/> + <line number="21" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/params_parser.rb" method="call"/> + <line number="177" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/flash.rb" method="call"/> + <line number="149" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/session/abstract_store.rb" method="call"/> + <line number="268" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/cookies.rb" method="call"/> + <line number="32" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/query_cache.rb" method="call"/> + <line number="28" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/connection_adapters/abstract/query_cache.rb" method="cache"/> + <line number="12" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/query_cache.rb" method="cache"/> + <line number="31" file="[GEM_ROOT]/gems/activerecord-3.0.0.rc/lib/active_record/query_cache.rb" method="call"/> + <line number="46" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/callbacks.rb" method="call"/> + <line number="410" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run_call_callbacks"/> + <line number="44" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/callbacks.rb" method="call"/> + <line number="107" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/sendfile.rb" method="call"/> + <line number="48" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/remote_ip.rb" method="call"/> + <line number="48" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/show_exceptions.rb" method="call"/> + <line number="13" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/rack/logger.rb" method="call"/> + <line number="17" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/runtime.rb" method="call"/> + <line number="72" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/cache/strategy/local_cache.rb" method="call"/> + <line number="11" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/lock.rb" method="call"/> + <line number="11" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/lock.rb" method="synchronize"/> + <line number="11" file="[GEM_ROOT]/gems/rack-1.2.1/lib/rack/lock.rb" method="call"/> + <line number="30" file="[GEM_ROOT]/gems/actionpack-3.0.0.rc/lib/action_dispatch/middleware/static.rb" method="call"/> + <line number="168" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/application.rb" method="call"/> + <line number="77" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/application.rb" method="send"/> + <line number="77" file="[GEM_ROOT]/gems/railties-3.0.0.rc/lib/rails/application.rb" method="method_missing"/> + <line number="636" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="call"/> + <line number="636" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="execute"/> + <line number="631" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="each"/> + <line number="631" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="execute"/> + <line number="597" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke_with_call_chain"/> + <line number="242" file="/Users/jdpace/.rvm/rubies/ree-1.8.7-2010.02/lib/ruby/1.8/monitor.rb" method="synchronize"/> + <line number="590" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke_with_call_chain"/> + <line number="583" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke"/> + <line number="2051" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="invoke_task"/> + <line number="2029" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="top_level"/> + <line number="2029" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="each"/> + <line number="2029" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="top_level"/> + <line number="2068" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="standard_exception_handling"/> + <line number="2023" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="top_level"/> + <line number="2001" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="run"/> + <line number="2068" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="standard_exception_handling"/> + <line number="1998" file="[GEM_ROOT]/gems/rake-0.8.7/lib/rake.rb" method="run"/> + <line number="31" file="[GEM_ROOT]/gems/rake-0.8.7/bin/rake" method=""/> + <line number="19" file="[GEM_ROOT]/bin/rake" method="load"/> + <line number="19" file="[GEM_ROOT]/bin/rake" method=""/> + </backtrace> + </error> + <server-environment> + <project-root>/path/to/sample/project</project-root> + <environment-name>development</environment-name> + </server-environment> +</notice> \ No newline at end of file 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..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) @@ -120,5 +131,40 @@ end end end - -end \ No newline at end of file + + 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_errs => false) + Mailer.should_not_receive(:err_notification) + Factory(:err, :app => app) + end + end + end + + 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/issue_tracker_spec.rb b/spec/models/issue_tracker_spec.rb new file mode 100644 index 0000000000..6b5f577724 --- /dev/null +++ b/spec/models/issue_tracker_spec.rb @@ -0,0 +1,5 @@ +# encoding: utf-8 +require 'spec_helper' + +describe IssueTracker do +end diff --git a/spec/models/notice_spec.rb b/spec/models/notice_spec.rb index e586c5e565..521bdf4341 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,43 +61,75 @@ @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 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 "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 @@ -105,7 +137,7 @@ @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) @@ -115,5 +147,5 @@ end end end - -end \ No newline at end of file + +end 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8793dde177..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. @@ -18,8 +19,8 @@ config.alias_example_to :fit, :focused => true config.before(:each) do - DatabaseCleaner.orm = "mongoid" - DatabaseCleaner.strategy = :truncation + DatabaseCleaner[:mongoid].strategy = :truncation DatabaseCleaner.clean end + config.include WebMock::API end \ No newline at end of file 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(/(<a href.*?(class="resolve").*?>)/)[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(/(<a href.*?(class="resolve").*?>)/)[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(/(<a href.*?(class="resolve").*?>)/)[0] + resolve_link.should_not =~ /data-confirm=/ + end + + end + +end