Commit 09f663fc665a493e68fa17a7c9dbd0361733aa4d

Authored by Cyril Mougel
2 parents a0af5530 cd44ad81
Exists in master and in 1 other branch production

Merge branch 'mongoid-3' into pr/289

Conflicts:
	app/views/apps/show.html.haml
Showing 228 changed files with 6164 additions and 2133 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 228 files displayed.

.gitignore
... ... @@ -13,3 +13,9 @@ config/newrelic.yml
13 13 *~
14 14 *.rbc
15 15 .DS_Store
  16 +*.rbx
  17 +bin
  18 +bundle
  19 +coverage
  20 +*#
  21 +.ruby-version
... ...
... ... @@ -1,4 +0,0 @@
1   ---colour
2   ---tty
3   ---drb
4   ---format documentation
.travis.yml
1 1 language: ruby
  2 +env:
  3 + - COVERAGE=true
2 4 rvm:
  5 + - 2.0.0
3 6 - 1.9.3
4   - - 1.9.2
5   - - 1.8.7
  7 + - rbx-19mode
6 8 services: mongodb
  9 +#script: ./script/rspec-queue-mongoid.rb --format progress spec
  10 +matrix:
  11 + allow_failures:
  12 + - rvm: rbx-19mode
7 13  
8 14 # To stop Travis from running tests for a new commit,
9 15 # add the following to your commit message: [ci skip]
... ...
CHANGELOG.md 0 → 100644
... ... @@ -0,0 +1,155 @@
  1 +## 0.3.0 - Not released Yet
  2 +
  3 +### Improvements
  4 +
  5 +- [#515][] Update to Mongoid 3.1 ([@arthurnn][])
  6 +
  7 +
  8 +[#515]: https://github.com/errbit/errbit/issues/515
  9 +
  10 +## 0.2.1 - Not released Yet
  11 +
  12 +### Improvements
  13 +
  14 +- [#552][] Limite size of asset on errbit ([@tscolari][])
  15 +
  16 +### Bug Fixes
  17 +
  18 +- [#558][] Avoid failure if you remote bitbucket_rest_api gem
  19 + ([@shingara][])
  20 +
  21 +[@shingara]: https://github.com/shingara
  22 +[@tscolari]: https://github.com/tscolari
  23 +
  24 +[#552]: https://github.com/errbit/errbit/issues/552
  25 +[#558]: https://github.com/errbit/errbit/issues/558
  26 +
  27 +## 0.2.0 - 2013-09-11
  28 +
  29 +### Improvements
  30 +
  31 +- Update some gems ([@shingara][])
  32 +- [#492][] Improve some Pjax call ([@nfedyashev][])
  33 +- [#428][] Add the support of Unfuddle Issue Tracker ([@parallel588][])
  34 +- Avoid to delete his own user ([@shingara][])
  35 +- [#456][] Avoid to delete admin access of current user logged ([@shingara][])
  36 +- [#253][] Refactor the Fingerprint generation ([@boblail][])
  37 +- [#508][] Merge comments to when you merge problems ([@shingara][])
  38 +- Update the Devise Gem to the last one ([@shingara][])
  39 +- [#524][] Add current user information on the notifer.js ([@roryf][])
  40 +- [#523][] Update javascript-stacktrace ([@aliscott][])
  41 +- [#516][] Add Jira Issue tracker ([@xenji][])
  42 +- [#512][] Add capabilities to configure the use of sendmail to send
  43 + email from Errbit ([@shingara][])
  44 +- [#532][] Use https link in Gravatar if you use errbit on https
  45 + ([@jeroenj][])
  46 +- [#536][] Order app by name by default ([@2called-chaos][])
  47 +- [#542][] Allow the MONGODB_URL env configuration about Mongodb ([@bacongobbler][])
  48 +- [#530][] Improve the flowdock notification ([@nfedyashev][])
  49 +- [#531][] Improve the HipChat notification message ([@brendonrapp][])
  50 +
  51 +
  52 +### Bug Fixes
  53 +
  54 +- [#343][] Fix the ical generation. ([@shingara][])
  55 +- [#503][] Fix issue on where the service_url choose never use. ([@nfedyashev][])
  56 +- [#506][] Fix issue on bitbucket issue tracker creation failed. ([@Gonzih][])
  57 +- [#514][] Add CDATA in xml return by Javascript. ([@mildavw][])
  58 +- [#517][] Javascript escape path from javascript Notifier. ([@roryf][])
  59 +- [#518][] Fix issue when you try launch task errbit:db:update_update_problem_attrs. ([@shingara][])
  60 +- [#526][] Fix issue of pagination after search. ([@shingara][])
  61 +- [#528][] Fix issue of action after search. ([@shingara][])
  62 +
  63 +## 0.1.0 - 2013-05-29
  64 +
  65 +### Improvements
  66 +
  67 +- [#474][] Improve message when access denied. ([@chadcf][])
  68 +- [#468][] Launch a repairDatabase in MongoDB database after launching
  69 + the clear_resolved task. ([@shingara][])
  70 +- Update gem of Mongoid to 2.7.1
  71 +- Update gem of Mongo to 1.8.5
  72 +- [#457][] Add task information about db:seed in Readme. ([@mildavw][])
  73 +- Add support of Ruby 2.0.0
  74 +- [#475][] Return a HTTP 422 status code when you try push notice with
  75 + bad api key. ([@shingara][])
  76 +- Return a 400 http status when you try put a notice without args.
  77 + ([@shingara][])
  78 +- [#486][] Add confirms box when you do massive action. ([@manuelmeurer][])
  79 +- [#487][] Add specific template to redmine notification with less useless data. ([@tvdeyen][])
  80 +
  81 +### Bug fixes
  82 +
  83 +- [#469][] Fix issue about the documentation of new heroku addons usage.
  84 + ([@adamjt][])
  85 +- [#455][] Avoid raising exception if you comment an exception and no
  86 + other user are define to received this comment. ([@alvarobp][])
  87 +- [#453][] Fix ruby 2.0.0 incompatibilities with gem ([@SamSaffron][])
  88 +- [#476][] Fix javascript notifier issue with IE8 ([@sdepold][])
  89 +- [#466][] Fix not see problem if octokit gem not define ([@tamaloa][])
  90 +- [#460][] Fix issue when you try see user with gravatar activate but no
  91 + email define to this user ([@ivanyv][])
  92 +- [#478][] Fix issue about calculation of statisque of problem after
  93 + merge ([@shingara][])
  94 +
  95 +<!-- Issue fix -->
  96 +
  97 +[#253]: https://github.com/errbit/errbit/issues/253
  98 +[#343]: https://github.com/errbit/errbit/issues/343
  99 +[#428]: https://github.com/errbit/errbit/issues/428
  100 +[#453]: https://github.com/errbit/errbit/issues/453
  101 +[#455]: https://github.com/errbit/errbit/issues/455
  102 +[#456]: https://github.com/errbit/errbit/issues/456
  103 +[#457]: https://github.com/errbit/errbit/issues/457
  104 +[#460]: https://github.com/errbit/errbit/issues/460
  105 +[#466]: https://github.com/errbit/errbit/issues/466
  106 +[#468]: https://github.com/errbit/errbit/issues/468
  107 +[#469]: https://github.com/errbit/errbit/issues/469
  108 +[#474]: https://github.com/errbit/errbit/issues/474
  109 +[#475]: https://github.com/errbit/errbit/issues/475
  110 +[#476]: https://github.com/errbit/errbit/issues/476
  111 +[#478]: https://github.com/errbit/errbit/issues/478
  112 +[#487]: https://github.com/errbit/errbit/issues/487
  113 +[#486]: https://github.com/errbit/errbit/issues/486
  114 +[#492]: https://github.com/errbit/errbit/issues/492
  115 +[#503]: https://github.com/errbit/errbit/issues/503
  116 +[#506]: https://github.com/errbit/errbit/issues/506
  117 +[#508]: https://github.com/errbit/errbit/issues/508
  118 +[#514]: https://github.com/errbit/errbit/issues/514
  119 +[#516]: https://github.com/errbit/errbit/issues/516
  120 +[#517]: https://github.com/errbit/errbit/issues/517
  121 +[#524]: https://github.com/errbit/errbit/issues/524
  122 +[#526]: https://github.com/errbit/errbit/issues/526
  123 +[#528]: https://github.com/errbit/errbit/issues/528
  124 +[#530]: https://github.com/errbit/errbit/issues/530
  125 +[#531]: https://github.com/errbit/errbit/issues/531
  126 +[#532]: https://github.com/errbit/errbit/issues/532
  127 +[#542]: https://github.com/errbit/errbit/issues/542
  128 +
  129 +<!-- Contributor on Errbit Thanks to all of them -->
  130 +
  131 +[@2called-chaos]: https://github.com/2called-chaos
  132 +[@Gonzih]: https://github.com/Gonzih
  133 +[@SamSaffron]: https://github.com/SamSaffron
  134 +[@adamjt]: https://github.com/adamjt
  135 +[@aliscott]: http://github.com/aliscott
  136 +[@alvarobp]: https://github.com/alvarobp
  137 +[@arthurnn]: https://github.com/arthurnn
  138 +[@bacongobbler]: https://github.com/bacongobbler
  139 +[@boblail]: https://github.com/boblail
  140 +[@brendonrapp]: https://github.com/brendonrapp
  141 +[@chadcf]: https://github.com/chadcf
  142 +[@ivanyv]: https://github.com/ivanyv
  143 +[@jeroenj]: https://github.com/jeroenj
  144 +[@manuelmeurer]: https://github.com/manuelmeurer
  145 +[@mildavw]: https://github.com/mildavw
  146 +[@mildavw]: https://github.com/mildavw
  147 +[@nfedyashev]: https://github.com/nfedyashev
  148 +[@parallel588]: https://github.com/parallel588
  149 +[@roryf]: https://github.com/roryf
  150 +[@sdepold]: https://github.com/sdepold
  151 +[@shingara]: https://github.com/shingara
  152 +[@tamaloa]: https://github.com/tamaloa
  153 +[@tvdeyen]: https://github.com/tvdeyen
  154 +[@williamn]: https://github.com/williamn
  155 +[@xenji]: https://github.com/xenji
... ...
CONTRIBUTORS.md 0 → 100644
... ... @@ -0,0 +1,75 @@
  1 +## 0.3.0 - Not released yet
  2 +
  3 +- [@arthurnn][]
  4 +- [@shingara][]
  5 +
  6 +[@shingara]: https://github.com/shingara
  7 +[@arthurnn]: https://github.com/arthurnn
  8 +
  9 +## 0.2.1 - Not released yet
  10 +
  11 +- [@shingara][]
  12 +- [@tscolari][]
  13 +
  14 +[@shingara]: https://github.com/shingara
  15 +[@tscolari]: https://github.com/tscolari
  16 +
  17 +## 0.2.0 - 2013-09-11
  18 +
  19 +- [@2called-chaos][]
  20 +- [@Gonzih][]
  21 +- [@aliscott][]
  22 +- [@arthurnn][]
  23 +- [@bacongobbler][]
  24 +- [@boblail][]
  25 +- [@brendonrapp][]
  26 +- [@jeroenj][]
  27 +- [@mildavw][]
  28 +- [@nfedyashev][]
  29 +- [@parallel588][]
  30 +- [@roryf][]
  31 +- [@shingara][]
  32 +- [@williamn][]
  33 +- [@xenji][]
  34 +
  35 +## 0.1.0 - 2013-05-29
  36 +
  37 +- [@manuelmeurer][]
  38 +- [@mildavw][]
  39 +- [@chadcf][]
  40 +- [@shingara][]
  41 +- [@tvdeyen][]
  42 +- [@adamjt][]
  43 +- [@alvarobp][]
  44 +- [@SamSaffron][]
  45 +- [@sdepold][]
  46 +- [@tamaloa][]
  47 +- [@ivanyv][]
  48 +
  49 +<!-- Contributor on Errbit Thanks to all of them -->
  50 +
  51 +[@2called-chaos]: https://github.com/2called-chaos
  52 +[@Gonzih]: https://github.com/Gonzih
  53 +[@SamSaffron]: https://github.com/SamSaffron
  54 +[@adamjt]: https://github.com/adamjt
  55 +[@aliscott]: http://github.com/aliscott
  56 +[@alvarobp]: https://github.com/alvarobp
  57 +[@arthurnn]: https://github.com/arthurnn
  58 +[@bacongobbler]: https://github.com/bacongobbler
  59 +[@boblail]: https://github.com/boblail
  60 +[@brendonrapp]: https://github.com/brendonrapp
  61 +[@chadcf]: https://github.com/chadcf
  62 +[@ivanyv]: https://github.com/ivanyv
  63 +[@jeroenj]: https://github.com/jeroenj
  64 +[@manuelmeurer]: https://github.com/manuelmeurer
  65 +[@mildavw]: https://github.com/mildavw
  66 +[@mildavw]: https://github.com/mildavw
  67 +[@nfedyashev]: https://github.com/nfedyashev
  68 +[@parallel588]: https://github.com/parallel588
  69 +[@roryf]: https://github.com/roryf
  70 +[@sdepold]: https://github.com/sdepold
  71 +[@shingara]: https://github.com/shingara
  72 +[@tamaloa]: https://github.com/tamaloa
  73 +[@tvdeyen]: https://github.com/tvdeyen
  74 +[@williamn]: https://github.com/williamn
  75 +[@xenji]: https://github.com/xenji
... ...
Gemfile
1 1 source 'http://rubygems.org'
2 2  
3   -gem 'rails', '3.2.8'
4   -gem 'mongoid', '~> 2.4.10'
5   -gem 'mongoid_rails_migrations'
6   -gem 'devise', '~> 1.5.3'
  3 +gem 'rails', '~> 3.2.13'
  4 +gem 'mongoid', '~> 3.1.4'
  5 +
  6 +gem 'mongoid_rails_migrations', '~> 1.0.1'
  7 +gem 'devise'
7 8 gem 'haml'
8   -gem 'htmlentities', "~> 4.3.0"
  9 +gem 'htmlentities'
9 10 gem 'rack-ssl', :require => 'rack/ssl' # force SSL
10 11  
11   -gem 'useragent', '~> 0.3.1'
12   -gem 'inherited_resources'
  12 +gem 'useragent'
  13 +gem 'decent_exposure'
  14 +gem 'strong_parameters'
13 15 gem 'SystemTimer', :platform => :ruby_18
14   -gem 'actionmailer_inline_css', "~> 1.3.0"
15   -gem 'kaminari'
16   -gem 'rack-ssl-enforcer'
  16 +gem 'actionmailer_inline_css'
  17 +gem 'kaminari', '>= 0.14.1'
  18 +gem 'rack-ssl-enforcer', :require => false
  19 +# fabrication 1.3.0 is last supporting ruby 1.8. Update when stop supporting this version too
17 20 gem 'fabrication', "~> 1.3.0" # Used for both tests and demo data
18   -gem 'rails_autolink', '~> 1.0.9'
  21 +gem 'rails_autolink'
19 22 # Please don't update hoptoad_notifier to airbrake.
20 23 # It's for internal use only, and we monkeypatch certain methods
21 24 gem 'hoptoad_notifier', "~> 2.4"
... ... @@ -35,68 +38,87 @@ gem &#39;pivotal-tracker&#39;
35 38 # Fogbugz
36 39 gem 'ruby-fogbugz', :require => 'fogbugz'
37 40 # Github Issues
38   -gem 'octokit', '~> 1.0.0'
  41 +gem 'octokit'
39 42 # Gitlab
40   -gem 'gitlab'
  43 +gem 'gitlab', :git => 'https://github.com/NARKOZ/gitlab.git'
41 44  
42 45 # Bitbucket Issues
43   -gem 'bitbucket_rest_api'
  46 +gem 'bitbucket_rest_api', :require => false
  47 +
  48 +# Unfuddle
  49 +gem "taskmapper", "~> 0.8.0"
  50 +gem "taskmapper-unfuddle", "~> 0.7.0"
  51 +
  52 +# Jira
  53 +gem 'jira-ruby', :require => 'jira'
44 54  
45 55 # Notification services
46 56 # ---------------------------------------
47   -# Campfire
48   -gem 'campy'
  57 +# Campfire ( We can't upgrade to 1.0 because drop support of ruby 1.8
  58 +gem 'campy', '0.1.3'
49 59 # Hipchat
50 60 gem 'hipchat'
51 61 # Google Talk
52   -gem 'xmpp4r'
  62 +gem 'xmpp4r', :require => ["xmpp4r", "xmpp4r/muc"]
53 63 # Hoiio (SMS)
54 64 gem 'hoi'
55 65 # Pushover (iOS Push notifications)
56 66 gem 'rushover'
  67 +# Hubot
  68 +gem 'httparty'
  69 +# Flowdock
  70 +gem 'flowdock'
57 71  
58 72 # Authentication
59 73 # ---------------------------------------
60 74 # GitHub OAuth
61 75 gem 'omniauth-github'
62 76  
63   -
64   -platform :ruby do
65   - gem 'mongo', '= 1.6.2'
66   - gem 'bson', '= 1.6.2'
67   - gem 'bson_ext', '= 1.6.2'
68   -end
69   -
70 77 gem 'ri_cal'
71 78 gem 'yajl-ruby', :require => "yajl"
72 79  
73 80 group :development, :test do
74 81 gem 'rspec-rails', '~> 2.6'
75 82 gem 'webmock', :require => false
76   - unless ENV["CI"]
77   - gem 'ruby-debug', :platform => :mri_18
78   - gem 'debugger', :platform => :mri_19
79   - gem 'pry-rails'
80   - end
  83 + gem 'airbrake', :require => false
  84 + gem 'ruby-debug', :platform => :mri_18
  85 + gem 'debugger', :platform => :mri_19
  86 + gem 'pry-rails'
81 87 # gem 'rpm_contrib'
82 88 # gem 'newrelic_rpm'
  89 + gem 'quiet_assets'
  90 +end
  91 +
  92 +group :development do
83 93 gem 'capistrano'
  94 +
  95 + # better errors
  96 + gem 'better_errors' , :platform => :ruby_19
  97 + gem 'binding_of_caller', :platform => :ruby_19
  98 + gem 'meta_request' , :platform => :ruby_19
  99 + gem 'foreman'
  100 +
  101 + # Use thin for development
  102 + gem 'thin', :group => :development, :platform => :ruby
  103 +
84 104 end
85 105  
86 106 group :test do
87   - gem 'capybara'
  107 + # Capybara 2.1.0 no more support 1.8.7 ruby version
  108 + gem 'capybara', "~> 2.0.1"
88 109 gem 'launchy'
89   - gem 'database_cleaner', '~> 0.6.0'
  110 + # DatabaseCleaner 1.0.0 drop the support of ruby 1.8.7
  111 + gem 'database_cleaner', '~> 0.9.0'
90 112 gem 'email_spec'
91   - gem 'timecop'
  113 + gem 'timecop', '0.6.1' # last version compatible to ruby 1.8
  114 + gem 'coveralls', :require => false
  115 + gem 'mongoid-rspec', :require => false
92 116 end
93 117  
94 118 group :heroku, :production do
95 119 gem 'unicorn'
96 120 end
97 121  
98   -# Use thin for development
99   -gem 'thin', :group => :development, :platform => :ruby
100 122  
101 123 # Gems used only for assets and not required
102 124 # in production environments by default.
... ... @@ -104,7 +126,10 @@ group :assets do
104 126 gem 'execjs'
105 127 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows
106 128 gem 'uglifier', '>= 1.0.3'
  129 + # We can't upgrade because not compatible to jquery >= 1.9.
  130 + # To do that, we need fix the rails.js
  131 + gem 'jquery-rails', '~> 2.1.4'
  132 + gem 'pjax_rails'
107 133 gem 'underscore-rails'
  134 + gem 'turbo-sprockets-rails3'
108 135 end
109   -
110   -gem 'turbo-sprockets-rails3'
... ...
Gemfile.lock
  1 +GIT
  2 + remote: https://github.com/NARKOZ/gitlab.git
  3 + revision: 7a00d38c53335010d2fb8a233247fe2c97338903
  4 + specs:
  5 + gitlab (2.2.0)
  6 + httparty
  7 +
1 8 GEM
2 9 remote: http://rubygems.org/
3 10 specs:
4 11 SystemTimer (1.2.3)
5   - actionmailer (3.2.8)
6   - actionpack (= 3.2.8)
7   - mail (~> 2.4.4)
8   - actionmailer_inline_css (1.3.1)
  12 + actionmailer (3.2.13)
  13 + actionpack (= 3.2.13)
  14 + mail (~> 2.5.3)
  15 + actionmailer_inline_css (1.5.3)
9 16 actionmailer (>= 3.0.0)
10 17 nokogiri (>= 1.4.4)
11 18 premailer (>= 1.7.1)
12   - actionpack (3.2.8)
13   - activemodel (= 3.2.8)
14   - activesupport (= 3.2.8)
  19 + actionpack (3.2.13)
  20 + activemodel (= 3.2.13)
  21 + activesupport (= 3.2.13)
15 22 builder (~> 3.0.0)
16 23 erubis (~> 2.7.0)
17 24 journey (~> 1.0.4)
18   - rack (~> 1.4.0)
  25 + rack (~> 1.4.5)
19 26 rack-cache (~> 1.2)
20 27 rack-test (~> 0.6.1)
21   - sprockets (~> 2.1.3)
22   - activemodel (3.2.8)
23   - activesupport (= 3.2.8)
  28 + sprockets (~> 2.2.1)
  29 + activemodel (3.2.13)
  30 + activesupport (= 3.2.13)
24 31 builder (~> 3.0.0)
25   - activerecord (3.2.8)
26   - activemodel (= 3.2.8)
27   - activesupport (= 3.2.8)
  32 + activerecord (3.2.13)
  33 + activemodel (= 3.2.13)
  34 + activesupport (= 3.2.13)
28 35 arel (~> 3.0.2)
29 36 tzinfo (~> 0.3.29)
30   - activeresource (3.2.8)
31   - activemodel (= 3.2.8)
32   - activesupport (= 3.2.8)
33   - activesupport (3.2.8)
34   - i18n (~> 0.6)
  37 + activeresource (3.2.13)
  38 + activemodel (= 3.2.13)
  39 + activesupport (= 3.2.13)
  40 + activesupport (3.2.13)
  41 + i18n (= 0.6.1)
35 42 multi_json (~> 1.0)
36   - addressable (2.3.2)
  43 + addressable (2.3.5)
  44 + airbrake (3.1.13)
  45 + builder
  46 + json
37 47 arel (3.0.2)
38   - bcrypt-ruby (3.0.1)
39   - bitbucket_rest_api (0.1.1)
  48 + bcrypt-ruby (3.1.1)
  49 + better_errors (0.9.0)
  50 + coderay (>= 1.0.0)
  51 + erubis (>= 2.6.6)
  52 + binding_of_caller (0.7.2)
  53 + debug_inspector (>= 0.0.1)
  54 + bitbucket_rest_api (0.1.2)
40 55 faraday (~> 0.8.1)
41 56 faraday_middleware (~> 0.8.1)
42 57 hashie (~> 1.2.0)
43 58 multi_json (~> 1.3)
44 59 nokogiri (~> 1.5.2)
45 60 simple_oauth
46   - bson (1.6.2)
47   - bson_ext (1.6.2)
48   - bson (~> 1.6.2)
49 61 builder (3.0.4)
  62 + callsite (0.0.11)
50 63 campy (0.1.3)
51 64 multi_json (~> 1.0)
52   - capistrano (2.13.5)
  65 + capistrano (2.15.5)
53 66 highline
54 67 net-scp (>= 1.0.0)
55 68 net-sftp (>= 2.0.0)
56 69 net-ssh (>= 2.0.14)
57 70 net-ssh-gateway (>= 1.1.0)
58   - capybara (1.1.2)
  71 + capybara (2.0.3)
59 72 mime-types (>= 1.16)
60 73 nokogiri (>= 1.3.3)
61 74 rack (>= 1.0.0)
62 75 rack-test (>= 0.5.4)
63 76 selenium-webdriver (~> 2.0)
64   - xpath (~> 0.1.4)
65   - childprocess (0.3.5)
66   - ffi (~> 1.0, >= 1.0.6)
67   - coderay (1.0.6)
  77 + xpath (~> 1.0.0)
  78 + childprocess (0.3.9)
  79 + ffi (~> 1.0, >= 1.0.11)
  80 + coderay (1.0.9)
  81 + colorize (0.5.8)
68 82 columnize (0.3.6)
69   - crack (0.3.1)
70   - css_parser (1.2.6)
  83 + coveralls (0.6.7)
  84 + colorize
  85 + multi_json (~> 1.3)
  86 + rest-client
  87 + simplecov (>= 0.7)
  88 + thor
  89 + crack (0.4.1)
  90 + safe_yaml (~> 0.9.0)
  91 + css_parser (1.3.4)
71 92 addressable
72   - rdoc
73   - daemons (1.1.8)
74   - database_cleaner (0.6.7)
75   - debugger (1.2.1)
  93 + daemons (1.1.9)
  94 + database_cleaner (0.9.1)
  95 + debug_inspector (0.0.2)
  96 + debugger (1.6.1)
76 97 columnize (>= 0.3.1)
77   - debugger-linecache (~> 1.1.1)
78   - debugger-ruby_core_source (~> 1.1.4)
79   - debugger-linecache (1.1.2)
80   - debugger-ruby_core_source (>= 1.1.1)
81   - debugger-ruby_core_source (1.1.4)
82   - devise (1.5.3)
  98 + debugger-linecache (~> 1.2.0)
  99 + debugger-ruby_core_source (~> 1.2.3)
  100 + debugger-linecache (1.2.0)
  101 + debugger-ruby_core_source (1.2.3)
  102 + decent_exposure (2.2.1)
  103 + devise (2.2.7)
83 104 bcrypt-ruby (~> 3.0)
84   - orm_adapter (~> 0.0.3)
85   - warden (~> 1.1)
86   - diff-lcs (1.1.3)
87   - email_spec (1.2.1)
  105 + orm_adapter (~> 0.1)
  106 + railties (~> 3.1)
  107 + warden (~> 1.2.1)
  108 + diff-lcs (1.2.4)
  109 + dotenv (0.8.0)
  110 + email_spec (1.5.0)
  111 + launchy (~> 2.1)
88 112 mail (~> 2.2)
89   - rspec (~> 2.0)
90 113 erubis (2.7.0)
91   - eventmachine (0.12.10)
  114 + eventmachine (1.0.3)
92 115 execjs (1.4.0)
93 116 multi_json (~> 1.0)
94 117 fabrication (1.3.2)
95   - faraday (0.8.4)
96   - multipart-post (~> 1.1)
  118 + faraday (0.8.8)
  119 + multipart-post (~> 1.2.0)
97 120 faraday_middleware (0.8.8)
98 121 faraday (>= 0.7.4, < 0.9)
99   - ffi (1.1.4)
100   - haml (3.1.6)
  122 + ffi (1.9.0)
  123 + flowdock (0.3.1)
  124 + httparty (~> 0.7)
  125 + multi_json
  126 + foreman (0.63.0)
  127 + dotenv (>= 0.7)
  128 + thor (>= 0.13.6)
  129 + haml (4.0.3)
  130 + tilt
101 131 happymapper (0.4.0)
102 132 libxml-ruby (~> 2.0)
103   - has_scope (0.5.1)
104 133 hashie (1.2.0)
105   - highline (1.6.15)
106   - hike (1.2.1)
107   - hipchat (0.4.1)
  134 + highline (1.6.19)
  135 + hike (1.2.3)
  136 + hipchat (0.11.0)
108 137 httparty
109 138 hoi (0.0.6)
110 139 httparty (> 0.6.0)
... ... @@ -113,152 +142,171 @@ GEM
113 142 activesupport
114 143 builder
115 144 htmlentities (4.3.1)
116   - httparty (0.9.0)
  145 + httparty (0.11.0)
117 146 multi_json (~> 1.0)
118   - multi_xml
119   - httpauth (0.1)
  147 + multi_xml (>= 0.5.2)
  148 + httpauth (0.2.0)
120 149 i18n (0.6.1)
121   - inherited_resources (1.3.1)
122   - has_scope (~> 0.5.0)
123   - responders (~> 0.6)
  150 + jira-ruby (0.1.2)
  151 + activesupport
  152 + oauth
  153 + railties
124 154 journey (1.0.4)
125   - json (1.7.5)
126   - jwt (0.1.5)
127   - multi_json (>= 1.0)
  155 + jquery-rails (2.1.4)
  156 + railties (>= 3.0, < 5.0)
  157 + thor (>= 0.14, < 2.0)
  158 + json (1.8.0)
  159 + jwt (0.1.8)
  160 + multi_json (>= 1.5)
128 161 kaminari (0.14.1)
129 162 actionpack (>= 3.0.0)
130 163 activesupport (>= 3.0.0)
131   - kgio (2.7.4)
132   - launchy (2.1.2)
  164 + kgio (2.8.0)
  165 + launchy (2.3.0)
133 166 addressable (~> 2.3)
134   - libv8 (3.3.10.4)
135   - libwebsocket (0.1.5)
136   - addressable
137   - libxml-ruby (2.3.3)
  167 + libv8 (3.16.14.3)
  168 + libxml-ruby (2.7.0)
138 169 lighthouse-api (2.0)
139 170 activeresource (>= 3.0.0)
140 171 activesupport (>= 3.0.0)
141 172 linecache (0.46)
142 173 rbx-require-relative (> 0.0.4)
143   - mail (2.4.4)
144   - i18n (>= 0.4.0)
  174 + mail (2.5.4)
145 175 mime-types (~> 1.16)
146 176 treetop (~> 1.4.8)
147   - method_source (0.7.1)
148   - mime-types (1.19)
149   - mongo (1.6.2)
150   - bson (~> 1.6.2)
151   - mongoid (2.4.10)
152   - activemodel (~> 3.1)
153   - mongo (~> 1.3)
  177 + meta_request (0.2.8)
  178 + callsite
  179 + rack-contrib
  180 + railties
  181 + method_source (0.8.2)
  182 + mime-types (1.24)
  183 + mongoid (3.1.4)
  184 + activemodel (~> 3.2)
  185 + moped (~> 1.4)
  186 + origin (~> 1.0)
154 187 tzinfo (~> 0.3.22)
155   - mongoid_rails_migrations (0.0.14)
156   - activesupport (>= 3.0.0)
  188 + mongoid-rspec (1.9.0)
  189 + mongoid (>= 3.0.1)
  190 + rake
  191 + rspec (>= 2.14)
  192 + mongoid_rails_migrations (1.0.1)
  193 + activesupport (>= 3.2.0)
157 194 bundler (>= 1.0.0)
158   - rails (>= 3.0.0)
159   - railties (>= 3.0.0)
160   - multi_json (1.3.6)
161   - multi_xml (0.5.1)
162   - multipart-post (1.1.5)
163   - net-scp (1.0.4)
164   - net-ssh (>= 1.99.1)
165   - net-sftp (2.0.5)
166   - net-ssh (>= 2.0.9)
167   - net-ssh (2.6.1)
168   - net-ssh-gateway (1.1.0)
169   - net-ssh (>= 1.99.1)
170   - nokogiri (1.5.5)
171   - oauth2 (0.8.0)
  195 + rails (>= 3.2.0)
  196 + railties (>= 3.2.0)
  197 + moped (1.5.1)
  198 + multi_json (1.7.9)
  199 + multi_xml (0.5.5)
  200 + multipart-post (1.2.0)
  201 + net-scp (1.1.2)
  202 + net-ssh (>= 2.6.5)
  203 + net-sftp (2.1.2)
  204 + net-ssh (>= 2.6.5)
  205 + net-ssh (2.6.8)
  206 + net-ssh-gateway (1.2.0)
  207 + net-ssh (>= 2.6.5)
  208 + nokogiri (1.5.10)
  209 + nokogiri-happymapper (0.5.7)
  210 + nokogiri (~> 1.5)
  211 + oauth (0.4.7)
  212 + oauth2 (0.8.1)
172 213 faraday (~> 0.8)
173 214 httpauth (~> 0.1)
174 215 jwt (~> 0.1.4)
175 216 multi_json (~> 1.0)
176 217 rack (~> 1.2)
177   - octokit (1.0.7)
  218 + octokit (1.18.0)
178 219 addressable (~> 2.2)
179 220 faraday (~> 0.8)
180 221 faraday_middleware (~> 0.8)
181 222 hashie (~> 1.2)
182 223 multi_json (~> 1.3)
183   - omniauth (1.1.1)
184   - hashie (~> 1.2)
  224 + omniauth (1.1.4)
  225 + hashie (>= 1.2, < 3)
185 226 rack
186   - omniauth-github (1.0.2)
  227 + omniauth-github (1.1.1)
187 228 omniauth (~> 1.0)
188 229 omniauth-oauth2 (~> 1.1)
189 230 omniauth-oauth2 (1.1.1)
190 231 oauth2 (~> 0.8.0)
191 232 omniauth (~> 1.0)
192   - orm_adapter (0.0.7)
  233 + origin (1.1.0)
  234 + orm_adapter (0.4.0)
193 235 oruen_redmine_client (0.0.1)
194 236 activeresource (>= 2.3.0)
195   - pivotal-tracker (0.5.4)
196   - builder
  237 + pivotal-tracker (0.5.10)
197 238 builder
198   - happymapper (>= 0.3.2)
  239 + crack
199 240 happymapper (>= 0.3.2)
200 241 nokogiri (>= 1.4.3)
201   - nokogiri (~> 1.4)
202   - rest-client (~> 1.6.0)
  242 + nokogiri (>= 1.5.5)
  243 + nokogiri-happymapper (>= 0.5.4)
203 244 rest-client (~> 1.6.0)
  245 + pjax_rails (0.3.4)
  246 + jquery-rails
204 247 polyglot (0.3.3)
205 248 premailer (1.7.3)
206 249 css_parser (>= 1.1.9)
207 250 htmlentities (>= 4.0.0)
208   - pry (0.9.9.6)
  251 + pry (0.9.12.2)
209 252 coderay (~> 1.0.5)
210   - method_source (~> 0.7.1)
211   - slop (>= 2.4.4, < 3)
212   - pry-rails (0.2.0)
213   - pry
214   - rack (1.4.1)
  253 + method_source (~> 0.8)
  254 + slop (~> 3.4)
  255 + pry-rails (0.3.2)
  256 + pry (>= 0.9.10)
  257 + quiet_assets (1.0.2)
  258 + railties (>= 3.1, < 5.0)
  259 + rack (1.4.5)
215 260 rack-cache (1.2)
216 261 rack (>= 0.4)
217   - rack-ssl (1.3.2)
  262 + rack-contrib (1.1.0)
  263 + rack (>= 0.9.1)
  264 + rack-ssl (1.3.3)
218 265 rack
219   - rack-ssl-enforcer (0.2.4)
  266 + rack-ssl-enforcer (0.2.5)
220 267 rack-test (0.6.2)
221 268 rack (>= 1.0)
222   - rails (3.2.8)
223   - actionmailer (= 3.2.8)
224   - actionpack (= 3.2.8)
225   - activerecord (= 3.2.8)
226   - activeresource (= 3.2.8)
227   - activesupport (= 3.2.8)
  269 + rails (3.2.13)
  270 + actionmailer (= 3.2.13)
  271 + actionpack (= 3.2.13)
  272 + activerecord (= 3.2.13)
  273 + activeresource (= 3.2.13)
  274 + activesupport (= 3.2.13)
228 275 bundler (~> 1.0)
229   - railties (= 3.2.8)
230   - rails_autolink (1.0.9)
231   - rails (~> 3.1)
232   - railties (3.2.8)
233   - actionpack (= 3.2.8)
234   - activesupport (= 3.2.8)
  276 + railties (= 3.2.13)
  277 + rails_autolink (1.1.0)
  278 + rails (> 3.1)
  279 + railties (3.2.13)
  280 + actionpack (= 3.2.13)
  281 + activesupport (= 3.2.13)
235 282 rack-ssl (~> 1.3.2)
236 283 rake (>= 0.8.7)
237 284 rdoc (~> 3.4)
238 285 thor (>= 0.14.6, < 2.0)
239   - raindrops (0.10.0)
240   - rake (0.9.2.2)
  286 + raindrops (0.11.0)
  287 + rake (10.1.0)
241 288 rbx-require-relative (0.0.9)
242   - rdoc (3.12)
  289 + rdoc (3.12.2)
243 290 json (~> 1.4)
244   - responders (0.9.2)
245   - railties (~> 3.1)
  291 + ref (1.0.5)
246 292 rest-client (1.6.7)
247 293 mime-types (>= 1.16)
248 294 ri_cal (0.8.8)
249   - rspec (2.11.0)
250   - rspec-core (~> 2.11.0)
251   - rspec-expectations (~> 2.11.0)
252   - rspec-mocks (~> 2.11.0)
253   - rspec-core (2.11.1)
254   - rspec-expectations (2.11.2)
255   - diff-lcs (~> 1.1.3)
256   - rspec-mocks (2.11.1)
257   - rspec-rails (2.11.0)
  295 + rspec (2.14.1)
  296 + rspec-core (~> 2.14.0)
  297 + rspec-expectations (~> 2.14.0)
  298 + rspec-mocks (~> 2.14.0)
  299 + rspec-core (2.14.5)
  300 + rspec-expectations (2.14.2)
  301 + diff-lcs (>= 1.1.3, < 2.0)
  302 + rspec-mocks (2.14.3)
  303 + rspec-rails (2.14.0)
258 304 actionpack (>= 3.0)
259 305 activesupport (>= 3.0)
260 306 railties (>= 3.0)
261   - rspec (~> 2.11.0)
  307 + rspec-core (~> 2.14.0)
  308 + rspec-expectations (~> 2.14.0)
  309 + rspec-mocks (~> 2.14.0)
262 310 ruby-debug (0.10.4)
263 311 columnize (>= 0.1)
264 312 ruby-debug-base (~> 0.10.4.0)
... ... @@ -267,52 +315,71 @@ GEM
267 315 ruby-fogbugz (0.1.1)
268 316 crack
269 317 rubyzip (0.9.9)
270   - rushover (0.1.1)
  318 + rushover (0.3.0)
271 319 json
272 320 rest-client
273   - selenium-webdriver (2.25.0)
  321 + safe_yaml (0.9.5)
  322 + selenium-webdriver (2.35.0)
274 323 childprocess (>= 0.2.5)
275   - libwebsocket (~> 0.1.3)
276 324 multi_json (~> 1.0)
277 325 rubyzip
278   - simple_oauth (0.1.9)
279   - slop (2.4.4)
280   - sprockets (2.1.3)
  326 + websocket (~> 1.0.4)
  327 + simple_oauth (0.2.0)
  328 + simplecov (0.7.1)
  329 + multi_json (~> 1.0)
  330 + simplecov-html (~> 0.7.1)
  331 + simplecov-html (0.7.1)
  332 + slop (3.4.6)
  333 + sprockets (2.2.2)
281 334 hike (~> 1.2)
  335 + multi_json (~> 1.0)
282 336 rack (~> 1.0)
283 337 tilt (~> 1.1, != 1.3.0)
284   - therubyracer (0.10.2)
285   - libv8 (~> 3.3.10)
286   - thin (1.4.1)
  338 + strong_parameters (0.2.1)
  339 + actionpack (~> 3.0)
  340 + activemodel (~> 3.0)
  341 + railties (~> 3.0)
  342 + taskmapper (0.8.0)
  343 + activeresource (~> 3.0)
  344 + activesupport (~> 3.0)
  345 + hashie (~> 1.2)
  346 + taskmapper-unfuddle (0.7.0)
  347 + addressable (~> 2.2)
  348 + taskmapper (~> 0.8)
  349 + therubyracer (0.12.0)
  350 + libv8 (~> 3.16.14.0)
  351 + ref
  352 + thin (1.5.1)
287 353 daemons (>= 1.0.9)
288 354 eventmachine (>= 0.12.6)
289 355 rack (>= 1.0.0)
290   - thor (0.16.0)
291   - tilt (1.3.3)
292   - timecop (0.3.5)
293   - treetop (1.4.10)
  356 + thor (0.18.1)
  357 + tilt (1.4.1)
  358 + timecop (0.6.1)
  359 + treetop (1.4.15)
294 360 polyglot
295 361 polyglot (>= 0.3.1)
296   - turbo-sprockets-rails3 (0.2.12)
297   - railties (>= 3.1.0, < 3.2.9)
  362 + turbo-sprockets-rails3 (0.3.9)
  363 + railties (> 3.2.8, < 4.0.0)
298 364 sprockets (>= 2.0.0)
299   - tzinfo (0.3.33)
300   - uglifier (1.2.7)
  365 + tzinfo (0.3.37)
  366 + uglifier (2.1.2)
301 367 execjs (>= 0.3.0)
302   - multi_json (~> 1.3)
303   - underscore-rails (1.4.2.1)
304   - unicorn (4.3.1)
  368 + multi_json (~> 1.0, >= 1.0.2)
  369 + underscore-rails (1.5.1)
  370 + unicorn (4.6.3)
305 371 kgio (~> 2.6)
306 372 rack
307 373 raindrops (~> 0.7)
308   - useragent (0.3.2)
309   - warden (1.2.1)
  374 + useragent (0.6.0)
  375 + warden (1.2.3)
310 376 rack (>= 1.0)
311   - webmock (1.8.7)
  377 + webmock (1.13.0)
312 378 addressable (>= 2.2.7)
313   - crack (>= 0.1.7)
  379 + crack (>= 0.3.2)
  380 + websocket (1.0.7)
314 381 xmpp4r (0.5)
315   - xpath (0.1.4)
  382 + xpath (1.0.0)
316 383 nokogiri (~> 1.3)
317 384 yajl-ruby (1.1.0)
318 385  
... ... @@ -321,53 +388,67 @@ PLATFORMS
321 388  
322 389 DEPENDENCIES
323 390 SystemTimer
324   - actionmailer_inline_css (~> 1.3.0)
  391 + actionmailer_inline_css
  392 + airbrake
  393 + better_errors
  394 + binding_of_caller
325 395 bitbucket_rest_api
326   - bson (= 1.6.2)
327   - bson_ext (= 1.6.2)
328   - campy
  396 + campy (= 0.1.3)
329 397 capistrano
330   - capybara
331   - database_cleaner (~> 0.6.0)
  398 + capybara (~> 2.0.1)
  399 + coveralls
  400 + database_cleaner (~> 0.9.0)
332 401 debugger
333   - devise (~> 1.5.3)
  402 + decent_exposure
  403 + devise
334 404 email_spec
335 405 execjs
336 406 fabrication (~> 1.3.0)
  407 + flowdock
  408 + foreman
  409 + gitlab!
337 410 haml
338 411 hipchat
339 412 hoi
340 413 hoptoad_notifier (~> 2.4)
341   - htmlentities (~> 4.3.0)
342   - inherited_resources
343   - kaminari
  414 + htmlentities
  415 + httparty
  416 + jira-ruby
  417 + jquery-rails (~> 2.1.4)
  418 + kaminari (>= 0.14.1)
344 419 launchy
345 420 lighthouse-api
346   - mongo (= 1.6.2)
347   - mongoid (~> 2.4.10)
348   - mongoid_rails_migrations
349   - octokit (~> 1.0.0)
  421 + meta_request
  422 + mongoid (~> 3.1.4)
  423 + mongoid-rspec
  424 + mongoid_rails_migrations (~> 1.0.1)
  425 + octokit
350 426 omniauth-github
351 427 oruen_redmine_client
352 428 pivotal-tracker
  429 + pjax_rails
353 430 pry-rails
  431 + quiet_assets
354 432 rack-ssl
355 433 rack-ssl-enforcer
356   - rails (= 3.2.8)
357   - rails_autolink (~> 1.0.9)
  434 + rails (~> 3.2.13)
  435 + rails_autolink
358 436 ri_cal
359 437 rspec-rails (~> 2.6)
360 438 ruby-debug
361 439 ruby-fogbugz
362 440 rushover
  441 + strong_parameters
  442 + taskmapper (~> 0.8.0)
  443 + taskmapper-unfuddle (~> 0.7.0)
363 444 therubyracer
364 445 thin
365   - timecop
  446 + timecop (= 0.6.1)
366 447 turbo-sprockets-rails3
367 448 uglifier (>= 1.0.3)
368 449 underscore-rails
369 450 unicorn
370   - useragent (~> 0.3.1)
  451 + useragent
371 452 webmock
372 453 xmpp4r
373 454 yajl-ruby
... ...
LICENSE
1   -Copyright (c) 2010 Jared Pace
  1 +Copyright (c) 2013 errbit team
2 2  
3 3 Permission is hereby granted, free of charge, to any person obtaining
4 4 a copy of this software and associated documentation files (the
... ... @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 17 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 18 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 19 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20   -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 20 \ No newline at end of file
  21 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
... ...
Procfile
1   -web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
  1 +web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
... ...
README.md
1   -# Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url]
  1 +# Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url] [![Coveralls][coveralls-img-url]][coveralls-url] [![Dependency Status][gemnasium-img-url]][gemnasium-url]
2 2  
3 3 [travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master
4 4 [travis-ci-url]: http://travis-ci.org/errbit/errbit
5   -[codeclimate-img-url]: https://codeclimate.com/badge.png
  5 +[codeclimate-img-url]: https://codeclimate.com/github/errbit/errbit.png
6 6 [codeclimate-url]: https://codeclimate.com/github/errbit/errbit
  7 +[coveralls-img-url]: https://coveralls.io/repos/errbit/errbit/badge.png?branch=master
  8 +[coveralls-url]:https://coveralls.io/r/errbit/errbit
  9 +[gemnasium-img-url]:https://gemnasium.com/errbit/errbit.png
  10 +[gemnasium-url]:https://gemnasium.com/errbit/errbit
  11 +
7 12  
8 13  
9 14 ### The open source, self-hosted error catcher
10 15  
11 16  
12 17 Errbit is a tool for collecting and managing errors from other applications.
13   -It is [Airbrake](http://airbrakeapp.com) (formerly known as Hoptoad) API compliant,
  18 +It is [Airbrake](http://airbrake.io) (formerly known as Hoptoad) API compliant,
14 19 so if you are already using Airbrake, you can just point the `airbrake` gem to your Errbit server.
15 20  
16 21  
... ... @@ -56,9 +61,9 @@ Errbit may be a good fit for you if:
56 61 * You want to add customer features to your error catcher
57 62 * You're crazy and love managing servers
58 63  
59   -If this doesn't sound like you, you should probably stick with [Airbrake](http://airbrakeapp.com).
60   -The [Thoughtbot](http://thoughtbot.com) guys offer great support for it and it is much more worry-free.
61   -They have a free package and even offer a *"Airbrake behind your firewall"* solution.
  64 +If this doesn't sound like you, you should probably stick with a hosted service such as
  65 +[Airbrake](http://airbrake.io).
  66 +
62 67  
63 68 Mailing List
64 69 ------------
... ... @@ -73,13 +78,19 @@ There is a demo available at [http://errbit-demo.herokuapp.com/](http://errbit-d
73 78 Email: demo@errbit-demo.herokuapp.com<br/>
74 79 Password: password
75 80  
  81 +# Requirement
  82 +
  83 +The list of requirement to install Errbit is :
  84 +
  85 + * Ruby 1.9.3 or higher
  86 + * MongoDB 2.2.0 or higher
  87 +
76 88 Installation
77 89 ------------
78 90  
79   -*Note*: This app is intended for people with experience deploying and maintining
  91 +*Note*: This app is intended for people with experience deploying and maintaining
80 92 Rails applications. If you're uncomfortable with any step below then Errbit is not
81   -for you. Checkout [Airbrake](http://airbrakeapp.com) from the guys over at
82   -[Thoughtbot](http://thoughtbot.com), which Errbit is based on.
  93 +for you.
83 94  
84 95 **Set up your local box or server(Ubuntu):**
85 96  
... ... @@ -87,7 +98,7 @@ for you. Checkout [Airbrake](http://airbrakeapp.com) from the guys over at
87 98  
88 99 ```bash
89 100 apt-get update
90   -apt-get install mongodb
  101 +apt-get install mongodb-10gen
91 102 ```
92 103  
93 104 * Install libxml and libcurl
... ... @@ -124,21 +135,19 @@ rake errbit:bootstrap
124 135 script/rails server
125 136 ```
126 137  
127   -**Deploying:**
128   -
129   - * Bootstrap Errbit. This will copy over config.yml and also seed the database.
130   -
131   -```bash
132   -rake errbit:bootstrap
133   -```
  138 +Deploying:
  139 +----------
134 140  
135   - * Update the deploy.rb file with information about your server
  141 + * Copy `config/deploy.example.rb` to `config/deploy.rb`
  142 + * Update the `deploy.rb` or `config.yml` file with information about your server
136 143 * Setup server and deploy
137 144  
138 145 ```bash
139   -cap deploy:setup deploy
  146 +cap deploy:setup deploy db:create_mongoid_indexes
140 147 ```
141 148  
  149 +(Note: The capistrano deploy script will automatically generate a unique secret token.)
  150 +
142 151 **Deploying to Heroku:**
143 152  
144 153 * Clone the repository
... ... @@ -146,24 +155,32 @@ cap deploy:setup deploy
146 155 ```bash
147 156 git clone http://github.com/errbit/errbit.git
148 157 ```
  158 + * Update `db/seeds.rb` with admin credentials for your initial login.
  159 +
  160 + * Run `bundle`
149 161  
150 162 * Create & configure for Heroku
151 163  
152 164 ```bash
153 165 gem install heroku
154   -heroku create example-errbit --stack cedar
155   -heroku addons:add mongolab:starter
  166 +heroku create example-errbit
  167 +# If you really want, you can define your stack and your buildpack. the default is good to us :
  168 +# heroku create example-errbit --stack cedar --buildpack https://github.com/heroku/heroku-buildpack-ruby.git
  169 +heroku addons:add mongolab:sandbox
156 170 heroku addons:add sendgrid:starter
157 171 heroku config:add HEROKU=true
  172 +heroku config:add SECRET_TOKEN="$(bundle exec rake secret)"
158 173 heroku config:add ERRBIT_HOST=some-hostname.example.com
159 174 heroku config:add ERRBIT_EMAIL_FROM=example@example.com
160 175 git push heroku master
161 176 ```
162 177  
163   - * Seed the DB (_NOTE_: No bootstrap task is used on Heroku!)
  178 + * Seed the DB (_NOTE_: No bootstrap task is used on Heroku!) and
  179 + create index
164 180  
165 181 ```bash
166 182 heroku run rake db:seed
  183 +heroku run rake db:mongoid:create_indexes
167 184 ```
168 185  
169 186 * If you are using a free database on Heroku, you may want to periodically clear resolved errors to free up space.
... ... @@ -199,6 +216,12 @@ heroku run rake db:seed
199 216 heroku addons:add deployhooks:http --url="http://YOUR_ERRBIT_HOST/deploys.txt?api_key=YOUR_API_KEY"
200 217 ```
201 218  
  219 + * You may also want to configure a different secret token for each deploy:
  220 +
  221 +```bash
  222 +heroku config:add SECRET_TOKEN=some-secret-token
  223 +```
  224 +
202 225 * Enjoy!
203 226  
204 227  
... ... @@ -313,6 +336,7 @@ When upgrading Errbit, please run:
313 336 git pull origin master # assuming origin is the github.com/errbit/errbit repo
314 337 bundle install
315 338 rake db:migrate
  339 +rake assets:precompile
316 340 ```
317 341  
318 342 If we change the way that data is stored, this will run any migrations to bring your database up to date.
... ... @@ -340,6 +364,20 @@ it will be displayed under the *User Details* tab:
340 364  
341 365 (This tab will be hidden if no user information is available.)
342 366  
  367 +Adding javascript errors notifications
  368 +--------------------------------------
  369 +
  370 +Errbit easily supports javascript errors notifications. You just need to add `config.js_notifier = true` to the errbit initializer in the rails app.
  371 +
  372 +```
  373 +Errbit.configure do |config|
  374 + config.host = 'YOUR-ERRBIT-HOST'
  375 + config.api_key = 'YOUR-PROJECT-API-KEY'
  376 + config.js_notifier = true
  377 +end
  378 +```
  379 +
  380 +Then get the `notifier.js` from `errbit/public/javascript/notifier.js` and add to `application.js` on your rails app or include `http://YOUR-ERRBIT-HOST/javascripts/notifier.js` on your `application.html.erb.`
343 381  
344 382 Issue Trackers
345 383 --------------
... ... @@ -387,9 +425,35 @@ card_type = Defect, status = Open, priority = Essential
387 425  
388 426 * Account is the host of your gitlab installation. i.e. **http://gitlab.example.com**
389 427 * To authenticate, Errbit uses token-based authentication. Get your API Key in your user settings (or create special user for this purpose)
390   -* You also need to provide project name (shortname) or ID (number) for issues to be created
391   -* **Currently (as of 3.0), Gitlab has 2000 character limit for issue description.** It is necessary to turn it off at your instance, because Errbit issues body is much longer. Please comment validation line in issue model in models folder https://github.com/gitlabhq/gitlabhq/blob/master/app/models/issue.rb#L10
  428 +* You also need to provide project ID (it needs to be Number) for issues to be created
  429 +
  430 +**Unfuddle Issues Integration**
  431 +
  432 +* Account is your unfuddle domain
  433 +* Username your unfuddle username
  434 +* Password your unfuddle password
  435 +* Project id the id of your project where your ticket is create
  436 +* Milestone id the id of your milestone where your ticket is create
  437 +
  438 +**Jira Issue Integration**
  439 +
  440 +* base_url the jira URL
  441 +* context_path Context Path (Just "/" if empty otherwise with leading slash)
  442 +* username HTTP Basic Auth User
  443 +* password HTTP Basic Auth Password
  444 +* project_id The project Key where the issue will be created
  445 +* account Assign to this user. If empty, Jira takes the project default.
  446 +* issue_component Website - Other
  447 +* issue_type Issue type
  448 +* issue_priority Priority
  449 +
  450 +Notification Service
  451 +--------------------
392 452  
  453 +**Flowdock Notification**
  454 +
  455 +Allow notification to [Flowdock](https://www.flowdock.com/). See
  456 +[complete documentation](docs/notifications/flowdock/index.md)
393 457  
394 458  
395 459 What if Errbit has an error?
... ... @@ -434,12 +498,28 @@ Solutions known to work are listed below:
434 498 </tr>
435 499 </table>
436 500  
  501 +Develop on Errbit
  502 +-----------------
  503 +
  504 +A guide can help on this way on [**Errbit Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md)
  505 +
  506 +## Other documentation
  507 +
  508 +* [All ENV variables availables to configure Errbit](docs/ENV-VARIABLES.md)
  509 +
437 510 TODO
438 511 ----
439 512  
440 513 * Add ability for watchers to be configured for types of notifications they should receive
441 514  
442 515  
  516 +People using Errbit
  517 +-------------------
  518 +
  519 +See our wiki page for a [list of people and companies around the world who use Errbit](https://github.com/errbit/errbit/wiki/People-using-Errbit).
  520 +Feel free to [edit this page](https://github.com/errbit/errbit/wiki/People-using-Errbit/_edit), and add your name and country to the list if you are using Errbit.
  521 +
  522 +
443 523 Special Thanks
444 524 --------------
445 525  
... ... @@ -448,10 +528,11 @@ Special Thanks
448 528 * [Nathan Broadbent (@ndbroadbent)](https://github.com/ndbroadbent) - Maintaining Errbit and contributing many features
449 529 * [Vasiliy Ermolovich (@nashby)](https://github.com/nashby) - Contributing and helping to resolve issues and pull requests
450 530 * [Marcin Ciunelis (@martinciu)](https://github.com/martinciu) - Helping to improve Errbit's architecture
  531 +* [Cyril Mougel (@shingara)](https://github.com/shingara) - Maintaining Errbit and contributing many features
451 532 * [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.
452   -* [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](http://airbrakeapp.com).
  533 +* [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](http://airbrake.io).
453 534  
454   -See the [contributors graph](https://github.com/errbit/errbit/graphs/contributors) for further details.
  535 +See the [contributors graph](https://github.com/errbit/errbit/graphs/contributors) for further details. You can see another list of Contributors by release version on [CONTRIBUTORS.md]
455 536  
456 537  
457 538 Contributing
... ... @@ -474,10 +555,11 @@ and make **optional** features configurable via `config/config.yml`.
474 555 * Add tests for it. This is important so we don't break it in a future version unintentionally.
475 556 * Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself we can ignore when we pull)
476 557 * Send us a pull request. Bonus points for topic branches.
  558 +* Add you on the CONTRIBUTORS.md file on the current release
477 559  
478 560  
479 561 Copyright
480 562 ---------
481 563  
482   -Copyright (c) 2010-2011 Jared Pace. See LICENSE for details.
  564 +Copyright (c) 2010-2013 Errbit Team. See LICENSE for details.
483 565  
... ...
app/assets/images/flowdock_create.png 0 → 100644

1.18 KB

app/assets/images/flowdock_goto.png 0 → 100644

1.18 KB

app/assets/images/flowdock_inactive.png 0 → 100644

1.1 KB

app/assets/images/hubot_create.png 0 → 100644

4.96 KB

app/assets/images/hubot_goto.png 0 → 100644

4.96 KB

app/assets/images/hubot_inactive.png 0 → 100644

4.78 KB

app/assets/images/jira_active.png 0 → 100644

1.97 KB

app/assets/images/jira_create.png 0 → 100644

1.97 KB

app/assets/images/jira_goto.png 0 → 100644

1.97 KB

app/assets/images/jira_inactive.png 0 → 100644

1.91 KB

app/assets/images/unfuddle_create.png 0 → 100644

2.29 KB

app/assets/images/unfuddle_goto.png 0 → 100644

2.29 KB

app/assets/images/unfuddle_inactive.png 0 → 100644

2.11 KB

app/assets/images/webhook_create.png 0 → 100644

3.16 KB

app/assets/images/webhook_goto.png 0 → 100644

3.16 KB

app/assets/images/webhook_inactive.png 0 → 100644

2.49 KB

app/assets/javascripts/application.js.erb
  1 +// We can't use jquery > 1.9
1 2 //= require jquery
2 3 //= require underscore
  4 +// This rails.js version is not like the original.
  5 +// We can't upgrade to the official version
3 6 //= require rails
4 7 //= require form
5 8 //= require jquery.pjax
... ...
app/assets/javascripts/errbit.js
... ... @@ -14,38 +14,28 @@ $(function() {
14 14  
15 15 bindRequiredPasswordMarks();
16 16  
17   - $('#watcher_name').live("click", function() {
18   - $(this).closest('form').find('.show').removeClass('show');
19   - $('#app_watchers_attributes_0_user_id').addClass('show');
20   - });
21   -
22   - $('#watcher_email').live("click", function() {
23   - $(this).closest('form').find('.show').removeClass('show');
24   - $('#app_watchers_attributes_0_email').addClass('show');
25   - });
26   -
27   - $('a.copy_config').live("click", function() {
  17 + // On page apps/:app_id/edit
  18 + $('a.copy_config').on("click", function() {
28 19 $('select.choose_other_app').show().focus();
29 20 });
30 21  
31   - $('select.choose_other_app').live("change", function() {
  22 + $('select.choose_other_app').on("change", function() {
32 23 var loc = window.location;
33 24 window.location.href = loc.protocol + "//" + loc.host + loc.pathname +
34 25 "?copy_attributes_from=" + $(this).val();
35 26 });
36 27  
37   - $('input[type=submit][data-action]').click(function() {
  28 + $('input[type=submit][data-action]').live('click', function() {
38 29 $(this).closest('form').attr('action', $(this).attr('data-action'));
39 30 });
40 31  
41 32 $('.notice-pagination').each(function() {
42   - $('.notice-pagination a').pjax('#content', { timeout: 2000});
43   - $('#content').bind('pjax:start', function() {
44   - $('.notice-pagination-loader').css("visibility", "visible");
45   - currentTab = $('.tab-bar ul li a.button.active').attr('rel');
46   - });
  33 + $.pjax.defaults = {timeout: 2000};
47 34  
48   - $('#content').bind('pjax:end', function() {
  35 + $('#content').pjax('.notice-pagination a').on('pjax:start', function() {
  36 + $('.notice-pagination-loader').css("visibility", "visible");
  37 + currentTab = $('.tab-bar ul li a.button.active').attr('rel');
  38 + }).on('pjax:end', function() {
49 39 activateTabbedPanels();
50 40 });
51 41 });
... ... @@ -83,7 +73,7 @@ $(function() {
83 73 function toggleProblemsCheckboxes() {
84 74 var checkboxToggler = $('#toggle_problems_checkboxes');
85 75  
86   - checkboxToggler.live("click", function() {
  76 + checkboxToggler.on("click", function() {
87 77 $('input[name^="problems"]').each(function() {
88 78 this.checked = checkboxToggler.get(0).checked;
89 79 });
... ... @@ -126,7 +116,7 @@ $(function() {
126 116 $('td.backtrace_separator').hide();
127 117 }
128 118 // Show external backtrace lines when clicking separator
129   - $('td.backtrace_separator span').live('click', show_external_backtrace);
  119 + $('td.backtrace_separator span').on('click', show_external_backtrace);
130 120 // Hide external backtrace on page load
131 121 hide_external_backtrace();
132 122  
... ...
app/assets/javascripts/form.js
... ... @@ -19,7 +19,8 @@ $(function(){
19 19 });
20 20  
21 21 function activateNestedForms() {
22   - $('.nested-wrapper').each(function(){
  22 + var wrapper = $('.nested-wrapper')
  23 + wrapper.each(function(){
23 24 var wrapper = $(this);
24 25  
25 26 makeNestedItemsDestroyable(wrapper);
... ... @@ -28,7 +29,7 @@ function activateNestedForms() {
28 29 addLink.click(appendNestedItem);
29 30 wrapper.append(addLink);
30 31 });
31   - $('.nested a.remove-nested').live('click',removeNestedItem);
  32 + wrapper.on('click','.nested a.remove-nested', removeNestedItem);
32 33 }
33 34  
34 35 function makeNestedItemsDestroyable(wrapper) {
... ... @@ -80,7 +81,7 @@ function activateTypeSelector(field_class, section_class) {
80 81 $('div.'+field_class+' > div.'+section_class).not('.chosen').find('input')
81 82 .attr('disabled','disabled').val('');
82 83  
83   - $('div.'+field_class+' input[name*=type]').live('click', function(){
  84 + $('div.'+field_class+' input[name*=type]').on('click', function(){
84 85 // Look for section in 'data-section', and fall back to 'value'
85 86 var chosen = $(this).data("section") || $(this).val();
86 87 var wrapper = $(this).closest('.nested');
... ...
app/assets/javascripts/jquery.alerts.js
... ... @@ -12,7 +12,7 @@
12 12 // $.jAlert( message, [title, callback] )
13 13 // $.jConfirm( message, [title, callback] )
14 14 // $.jPrompt( message, [value, title, callback] )
15   -//
  15 +//
16 16 // History:
17 17 //
18 18 // 1.00 - Released (29 December 2008)
... ... @@ -22,16 +22,16 @@
22 22 // 1.2 - global methods removed.
23 23 //
24 24 // License:
25   -//
  25 +//
26 26 // This plugin is dual-licensed under the GNU General Public License and the MIT License and
27   -// is copyright 2008 A Beautiful Site, LLC.
  27 +// is copyright 2008 A Beautiful Site, LLC.
28 28 //
29 29 (function($) {
30   -
  30 +
31 31 $.alerts = {
32   -
  32 +
33 33 // These properties can be read/written by accessing $.alerts.propertyName from your scripts at any time
34   -
  34 +
35 35 verticalOffset: -75, // vertical offset of the dialog from center screen, in pixels
36 36 horizontalOffset: 0, // horizontal offset of the dialog from center screen, in pixels/
37 37 repositionOnResize: true, // re-centers the dialog on window resize
... ... @@ -46,37 +46,37 @@
46 46 confirm: 'Confirm',
47 47 prompt: 'Prompt'
48 48 },
49   -
  49 +
50 50 // Public methods
51   -
  51 +
52 52 alert: function(message, title, callback) {
53 53 if (! title) title = $.alerts.titles.alert;
54 54 $.alerts._show(title, message, null, 'alert', function(result) {
55 55 if (callback) callback(result);
56 56 });
57 57 },
58   -
  58 +
59 59 confirm: function(message, title, callback) {
60 60 if (! title) title = $.alerts.titles.confirm;
61 61 $.alerts._show(title, message, null, 'confirm', function(result) {
62 62 if (callback) callback(result);
63 63 });
64 64 },
65   -
  65 +
66 66 prompt: function(message, value, title, callback) {
67 67 if (! title) title = $.alerts.titles.prompt;
68 68 $.alerts._show(title, message, value, 'prompt', function(result) {
69 69 if(callback) callback(result);
70 70 });
71 71 },
72   -
  72 +
73 73 // Private methods
74   -
  74 +
75 75 _show: function(title, msg, value, type, callback) {
76   -
  76 +
77 77 $.alerts._hide();
78 78 $.alerts._overlay('show');
79   -
  79 +
80 80 $("BODY").append(
81 81 '<div id="popup_container">' +
82 82 '<h1 id="popup_title"></h1>' +
... ... @@ -84,32 +84,34 @@
84 84 '<div id="popup_message"></div>' +
85 85 '</div>' +
86 86 '</div>');
87   -
  87 +
88 88 if( $.alerts.dialogClass ) $("#popup_container").addClass($.alerts.dialogClass);
89   -
  89 +
90 90 // IE6 Fix
91   - var pos = ($.browser.msie && parseInt($.browser.version, 10) <= 6 ) ? 'absolute' : 'fixed';
92   -
  91 + // No more $.browser in Jquery > 1,9 and not support IE 6
  92 + // var pos = ($.browser.msie && parseInt($.browser.version, 10) <= 6 ) ? 'absolute' : 'fixed';
  93 + var pos = 'fixed';
  94 +
93 95 $("#popup_container").css({
94 96 position: pos,
95 97 zIndex: 99999,
96 98 padding: 0,
97 99 margin: 0
98 100 });
99   -
  101 +
100 102 $("#popup_title").text(title);
101 103 $("#popup_content").addClass(type);
102 104 $("#popup_message").text(msg);
103 105 $("#popup_message").html( $("#popup_message").text().replace(/\n/g, '<br />') );
104   -
  106 +
105 107 $("#popup_container").css({
106 108 minWidth: $("#popup_container").outerWidth(),
107 109 maxWidth: $("#popup_container").outerWidth()
108 110 });
109   -
  111 +
110 112 $.alerts._reposition();
111 113 $.alerts._maintainPosition(true);
112   -
  114 +
113 115 switch( type ) {
114 116 case 'alert':
115 117 $("#popup_message").after('<div id="popup_panel"><input type="button" value="' + $.alerts.okButton + '" id="popup_ok" /></div>');
... ... @@ -158,20 +160,20 @@
158 160 break;
159 161 default: break;
160 162 }
161   -
  163 +
162 164 // Make draggable
163 165 if ($.alerts.draggable && $.fn.draggable) {
164 166 $("#popup_container").draggable({ handle: $("#popup_title") });
165 167 $("#popup_title").css({ cursor: 'move' });
166 168 }
167 169 },
168   -
  170 +
169 171 _hide: function() {
170 172 $("#popup_container").remove();
171 173 $.alerts._overlay('hide');
172 174 $.alerts._maintainPosition(false);
173 175 },
174   -
  176 +
175 177 _overlay: function(status) {
176 178 switch( status ) {
177 179 case 'show':
... ... @@ -194,23 +196,23 @@
194 196 default: break;
195 197 }
196 198 },
197   -
  199 +
198 200 _reposition: function() {
199 201 var top = (($(window).height() / 2) - ($("#popup_container").outerHeight() / 2)) + $.alerts.verticalOffset;
200 202 var left = (($(window).width() / 2) - ($("#popup_container").outerWidth() / 2)) + $.alerts.horizontalOffset;
201 203 if( top < 0 ) top = 0;
202 204 if( left < 0 ) left = 0;
203   -
  205 +
204 206 // IE6 fix
205   - if( $.browser.msie && parseInt($.browser.version, 10) <= 6 ) top = top + $(window).scrollTop();
206   -
  207 + // if( $.browser.msie && parseInt($.browser.version, 10) <= 6 ) top = top + $(window).scrollTop();
  208 +
207 209 $("#popup_container").css({
208 210 top: top + 'px',
209 211 left: left + 'px'
210 212 });
211 213 $("#popup_overlay").height( $(document).height() );
212 214 },
213   -
  215 +
214 216 _maintainPosition: function(status) {
215 217 if( $.alerts.repositionOnResize ) {
216 218 switch(status) {
... ... @@ -224,7 +226,7 @@
224 226 }
225 227 }
226 228 }
227   -
  229 +
228 230 };
229   -
230   -})(jQuery);
231 231 \ No newline at end of file
  232 +
  233 +})(jQuery);
... ...
app/assets/javascripts/jquery.js
... ... @@ -1,18 +0,0 @@
1   -/*!
2   - * jQuery JavaScript Library v1.6.2
3   - * http://jquery.com/
4   - *
5   - * Copyright 2011, John Resig
6   - * Dual licensed under the MIT or GPL Version 2 licenses.
7   - * http://jquery.org/license
8   - *
9   - * Includes Sizzle.js
10   - * http://sizzlejs.com/
11   - * Copyright 2011, The Dojo Foundation
12   - * Released under the MIT, BSD, and GPL Licenses.
13   - *
14   - * Date: Thu Jun 30 14:16:56 2011 -0400
15   - */
16   -(function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bZ(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bY(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bC.test(a)?d(a,e):bY(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)bY(a+"["+e+"]",b[e],c,d);else d(a,b)}function bX(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bR,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bX(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bX(a,c,d,e,"*",g));return l}function bW(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bN),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bA(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bv:bw;if(d>0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)f.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function bg(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;i<s.length;i++)g=s[i],g.origType.replace(x,"")===a.type?q.push(g.selector):s.splice(i--,1);e=f(a.target).closest(q,a.currentTarget);for(j=0,k=e.length;j<k;j++){m=e[j];for(i=0;i<s.length;i++){g=s[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,d=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,d=f(a.relatedTarget).closest(g.selector)[0],d&&f.contains(h,d)&&(d=h);(!d||d!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){e=p[j];if(c&&e.level>c)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.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 d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.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(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,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"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.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=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b){if(H)return H.call(b,a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g="done fail isResolved isRejected promise then always pipe".split(" "),h=[].slice;f.extend({_Deferred:function(){var a=[],b,c,d,e={done:function(){if(!d){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=f.type(i),j==="array"?e.done.apply(e,i):j==="function"&&a.push(i);k&&e.resolveWith(k[0],k[1])}return this},resolveWith:function(e,f){if(!d&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(e,f)}finally{b=[e,f],c=0}}return this},resolve:function(){e.resolveWith(this,arguments);return this},isResolved:function(){return!!c||!!b},cancel:function(){d=1,a=[];return this}};return e},Deferred:function(a){var b=f._Deferred(),c=f._Deferred(),d;f.extend(b,{then:function(a,c){b.done(a).fail(c);return this},always:function(){return b.done.apply(b,arguments).fail.apply(this,arguments)},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,pipe:function(a,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[c,"reject"]},function(a,c){var e=c[0],g=c[1],h;f.isFunction(e)?b[a](function(){h=e.apply(this,arguments),h&&f.isFunction(h.promise)?h.promise().then(d.resolve,d.reject):d[g](h)}):b[a](d[g])})}).promise()},promise:function(a){if(a==null){if(d)return d;d=a={}}var c=g.length;while(c--)a[g[c]]=b[g[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c<d;c++)b[c]&&f.isFunction(b[c].promise)?b[c].promise().then(i(c),g.reject):--e;e||g.resolveWith(g,b)}else g!==a&&g.resolveWith(g,d?[a]:[]);return g.promise()}}),f.support=function(){var a=c.createElement("div"),b=c.documentElement,d,e,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.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?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h<i;h++)g=e[h].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),k(this[0],g,d[g]))}}return d}if(typeof a=="object")return this.each(function(){f.data(this,a)});var j=a.split(".");j[1]=j[1]?"."+j[1]:"";if(c===b){d=this.triggerHandler("getData"+j[1]+"!",[j[0]]),d===b&&this.length&&(d=f.data(this[0],a),d=k(this[0],a,d));return d===b&&j[1]?this.data(j[0]):d}return this.each(function(){var b=f(this),d=[j[0],c];b.triggerHandler("setData"+j[1]+"!",d),f.data(this,a,c),b.triggerHandler("changeData"+j[1]+"!",d)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,c){a&&(c=(c||"fx")+"mark",f.data(a,c,(f.data(a,c,b,!0)||0)+1,!0))},_unmark:function(a,c,d){a!==!0&&(d=c,c=a,a=!1);if(c){d=d||"fx";var e=d+"mark",g=a?0:(f.data(c,e,b,!0)||1)-1;g?f.data(c,e,g,!0):(f.removeData(c,e,!0),m(c,d,"mark"))}},queue:function(a,c,d){if(a){c=(c||"fx")+"queue";var e=f.data(a,c,b,!0);d&&(!e||f.isArray(d)?e=f.data(a,c,f.makeArray(d),!0):e.push(d));return e||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e;d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),d.call(a,function(){f.dequeue(a,b)})),c.length||(f.removeData(a,b+"queue",!0),m(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){f.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f._Deferred(),!0))h++,l.done(m);m();return d.promise()}});var n=/[\n\t\r]/g,o=/\s+/,p=/\r/g,q=/^(?:button|input)$/i,r=/^(?:button|input|object|select|textarea)$/i,s=/^a(?:rea)?$/i,t=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,u=/\:|^on/,v,w;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(o);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(o);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(n," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(o);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(n," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h<i;h++){var j=e[h];if(j.selected&&(f.support.optDisabled?!j.disabled:j.getAttribute("disabled")===null)&&(!j.parentNode.disabled||!f.nodeName(j.parentNode,"optgroup"))){b=f(j).val();if(g)return b;d.push(b)}}if(g&&!d.length&&e.length)return f(e[c]).val();return d},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.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("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))f.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=f.event.special[h]||{};for(j=e||0;j<p.length;j++){q=p[j];if(d.guid===q.guid){if(l||n.test(q.namespace))e==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(e!=null)break}}if(p.length===0||e!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&f.removeEvent(a,h,s.handle),g=null,delete t[h]}if(f.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,f.isEmptyObject(s)&&f.removeData(a,b,!0)}}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){var h=c.type||c,i=[],j;h.indexOf("!")>=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.
17   -shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h<i;h++){var j=d[h];if(e||c.namespace_re.test(j.namespace)){c.handler=j.handler,c.data=j.data,c.handleObj=j;var k=j.handler.apply(this,g);k!==b&&(c.result=k,k===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[f.expando])return a;var d=a;a=f.Event(d);for(var e=this.props.length,g;e;)g=this.props[--e],a[g]=d[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=a.target.ownerDocument||c,i=h.documentElement,j=h.body;a.pageX=a.clientX+(i&&i.scrollLeft||j&&j.scrollLeft||0)-(i&&i.clientLeft||j&&j.clientLeft||0),a.pageY=a.clientY+(i&&i.scrollTop||j&&j.scrollTop||0)-(i&&i.clientTop||j&&j.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:f.proxy,special:{ready:{setup:f.bindReady,teardown:f.noop},live:{add:function(a){f.event.add(this,N(a.origType,a.selector),f.extend({},a,{handler:M,guid:a.handler.guid}))},remove:function(a){f.event.remove(this,N(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!this.preventDefault)return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?E:D):this.type=a,b&&f.extend(this,b),this.timeStamp=f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=E;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=E;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=E,this.stopPropagation()},isDefaultPrevented:D,isPropagationStopped:D,isImmediatePropagationStopped:D};var F=function(a){var b=a.relatedTarget,c=!1,d=a.type;a.type=a.data,b!==this&&(b&&(c=f.contains(this,b)),c||(f.event.handle.apply(this,arguments),a.type=d))},G=function(a){a.type=a.data,f.event.handle.apply(this,arguments)};f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={setup:function(c){f.event.add(this,b,c&&c.selector?G:F,a)},teardown:function(a){f.event.remove(this,b,a&&a.selector?G:F)}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(a,b){if(!f.nodeName(this,"form"))f.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&f(b).closest("form").length&&K("submit",this,arguments)}),f.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&f(b).closest("form").length&&a.keyCode===13&&K("submit",this,arguments)});else return!1},teardown:function(a){f.event.remove(this,".specialSubmit")}});if(!f.support.changeBubbles){var H,I=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i<j;i++)f.event.add(this[i],a,g,d);return this}}),f.fn.extend({unbind:function(a,b){if(typeof a=="object"&&!a.preventDefault)for(var c in a)this.unbind(c,a[c]);else for(var d=0,e=this.length;d<e;d++)f.event.remove(this[d],a,b);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f.data(this,"lastToggle"+a.guid)||0)%d;f.data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var L={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};f.each(["live","die"],function(a,c){f.fn[c]=function(a,d,e,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:f(this.context);if(typeof a=="object"&&!a.preventDefault){for(var o in a)n[c](o,d,a[o],m);return this}if(c==="die"&&!a&&g&&g.charAt(0)==="."){n.unbind(g);return this}if(d===!1||f.isFunction(d))e=d||D,d=b;a=(a||"").split(" ");while((h=a[i++])!=null){j=x.exec(h),k="",j&&(k=j[0],h=h.replace(x,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,L[h]?(a.push(L[h]+k),h=h+k):h=(L[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)f.event.add(n[p],"live."+N(h,m),{data:d,selector:m,handler:e,origType:h,origHandler:e,preType:l});else n.unbind("live."+N(h,m),e)}return this}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){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<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;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(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else 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]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(!f)g=o=!0;else if(f===!0)continue}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("parentNode",b,f,a,e,c)},"~":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("previousSibling",b,f,a,e,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=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]=d++;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){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},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 b<c[3]-0},gt:function(a,b,c){return b>c[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<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=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(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c<f;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){if(a===b){g=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",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 href='#'></a>",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="<p class='TEST'></p>";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;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";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;g<h;g++)k(a,f[g],d);return k.filter(e,d)};f.find=k,f.expr=k.selectors,f.expr[":"]=f.expr.filters,f.unique=k.uniqueSort,f.text=k.getText,f.isXMLDoc=k.isXML,f.contains=k.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d<e;d++)i=a[d],j[i]||(j[i]=T.test(i)?f(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(l?l.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},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 X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/<tbody/i,ba=/<|&#?\w+;/,bb=/<(?:script|object|embed|option|style)/i,bc=/checked\s*(?:[^=]|=\s*.checked.)/i,bd=/\/(java|ecma)script/i,be=/^\s*<!(?:\[CDATA\[|\-\-)/,bf={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(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 f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(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(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(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=f(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,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.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 f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bc.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bg(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bm)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i;b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof a[0]=="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!bb.test(a[0])&&(f.support.checkClone||!bc.test(a[0]))&&(g=!0,h=f.fragments[a[0]],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[a[0]]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j
18   -)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1></$2>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bl(k[i]);else bl(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||bd.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.expando,g=f.event.special,h=f.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&f.noData[j.nodeName.toLowerCase()])continue;c=j[f.expando];if(c){b=d[c]&&d[c][e];if(b&&b.events){for(var k in b.events)g[k]?f.event.remove(j,k):f.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[f.expando]:j.removeAttribute&&j.removeAttribute(f.expando),delete d[c]}}}});var bn=/alpha\([^)]*\)/i,bo=/opacity=([^)]*)/,bp=/([A-Z]|^ms)/g,bq=/^-?\d+(?:px)?$/i,br=/^-?\d/,bs=/^[+\-]=/,bt=/[^+\-\.\de]+/g,bu={position:"absolute",visibility:"hidden",display:"block"},bv=["Left","Right"],bw=["Top","Bottom"],bx,by,bz;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bx(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d;if(h==="number"&&isNaN(d)||d==null)return;h==="string"&&bs.test(d)&&(d=+d.replace(bt,"")+parseFloat(f.css(a,c)),h="number"),h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bx)return bx(a,c)},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]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bA(a,b,d);f.swap(a,bu,function(){e=bA(a,b,d)});return e}},set:function(a,b){if(!bq.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.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}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.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?f("<div>").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[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":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.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(c||!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 cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cs(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cr("hide",3),a,b,c);for(var d=0,e=this.length;d<e;d++)if(this[d].style){var g=f.css(this[d],"display");g!=="none"&&!f._data(this[d],"olddisplay")&&f._data(this[d],"olddisplay",g)}for(d=0;d<e;d++)this[d].style&&(this[d].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cr("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return this[e.queue===!1?"each":"queue"](function(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(f.support.inlineBlockNeedsLayout?(j=cs(this.nodeName),j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)):this.style.display="inline-block"))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)k=new f.fx(this,b,i),h=a[i],cj.test(h)?k[h==="toggle"?d?"show":"hide":h]():(l=ck.exec(h),m=k.cur(),l?(n=parseFloat(l[2]),o=l[3]||(f.cssNumber[i]?"":"px"),o!=="px"&&(f.style(this,i,(n||1)+o),m=(n||1)/k.cur()*m,f.style(this,i,m+o)),l[1]&&(n=(l[1]==="-="?-1:1)*n+m),k.custom(m,n,o)):k.custom(m,h,""));return!0})},stop:function(a,b){a&&this.queue([]),this.each(function(){var a=f.timers,c=a.length;b||f._unmark(!0,this);while(c--)a[c].elem===this&&(b&&a[c](!0),a.splice(c,1))}),b||this.dequeue();return this}}),f.each({slideDown:cr("show",1),slideUp:cr("hide",1),slideToggle:cr("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default,d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue!==!1?f.dequeue(this):a!==!1&&f._unmark(this)};return d},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||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.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=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function h(a){return d.step(a)}var d=this,e=f.fx,g;this.startTime=cn||cp(),this.start=a,this.end=b,this.unit=c||this.unit||(f.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,h.elem=this.elem,h()&&f.timers.push(h)&&!cl&&(co?(cl=!0,g=function(){cl&&(co(g),e.tick())},co(g)):cl=setInterval(e.tick,e.interval))},show:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=cn||cp(),c=!0,d=this.elem,e=this.options,g,h;if(a||b>=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b<a.length;++b)a[b]()||a.splice(b--,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cl),cl=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var ct=/^t(?:able|d|h)$/i,cu=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cv(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!ct.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";f.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),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.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):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window);
19 0 \ No newline at end of file
app/assets/javascripts/jquery.pjax.js
... ... @@ -1,264 +0,0 @@
1   -// jquery.pjax.js
2   -// copyright chris wanstrath
3   -// https://github.com/defunkt/jquery-pjax
4   -
5   -(function($){
6   -
7   -// When called on a link, fetches the href with ajax into the
8   -// container specified as the first parameter or with the data-pjax
9   -// attribute on the link itself.
10   -//
11   -// Tries to make sure the back button and ctrl+click work the way
12   -// you'd expect.
13   -//
14   -// Accepts a jQuery ajax options object that may include these
15   -// pjax specific options:
16   -//
17   -// container - Where to stick the response body. Usually a String selector.
18   -// $(container).html(xhr.responseBody)
19   -// push - Whether to pushState the URL. Defaults to true (of course).
20   -// replace - Want to use replaceState instead? That's cool.
21   -//
22   -// For convenience the first parameter can be either the container or
23   -// the options object.
24   -//
25   -// Returns the jQuery object
26   -$.fn.pjax = function( container, options ) {
27   - if ( options )
28   - options.container = container
29   - else
30   - options = $.isPlainObject(container) ? container : {container:container}
31   -
32   - // We can't persist $objects using the history API so we must use
33   - // a String selector. Bail if we got anything else.
34   - if ( options.container && typeof options.container !== 'string' ) {
35   - throw "pjax container must be a string selector!"
36   - return false
37   - }
38   -
39   - return this.live('click', function(event){
40   - // Middle click, cmd click, and ctrl click should open
41   - // links in a new tab as normal.
42   - if ( event.which > 1 || event.metaKey )
43   - return true
44   -
45   - var defaults = {
46   - url: this.href,
47   - container: $(this).attr('data-pjax'),
48   - clickedElement: $(this),
49   - fragment: null
50   - }
51   -
52   - $.pjax($.extend({}, defaults, options))
53   -
54   - event.preventDefault()
55   - })
56   -}
57   -
58   -
59   -// Loads a URL with ajax, puts the response body inside a container,
60   -// then pushState()'s the loaded URL.
61   -//
62   -// Works just like $.ajax in that it accepts a jQuery ajax
63   -// settings object (with keys like url, type, data, etc).
64   -//
65   -// Accepts these extra keys:
66   -//
67   -// container - Where to stick the response body. Must be a String.
68   -// $(container).html(xhr.responseBody)
69   -// push - Whether to pushState the URL. Defaults to true (of course).
70   -// replace - Want to use replaceState instead? That's cool.
71   -//
72   -// Use it just like $.ajax:
73   -//
74   -// var xhr = $.pjax({ url: this.href, container: '#main' })
75   -// console.log( xhr.readyState )
76   -//
77   -// Returns whatever $.ajax returns.
78   -var pjax = $.pjax = function( options ) {
79   - var $container = $(options.container),
80   - success = options.success || $.noop
81   -
82   - // We don't want to let anyone override our success handler.
83   - delete options.success
84   -
85   - // We can't persist $objects using the history API so we must use
86   - // a String selector. Bail if we got anything else.
87   - if ( typeof options.container !== 'string' )
88   - throw "pjax container must be a string selector!"
89   -
90   - options = $.extend(true, {}, pjax.defaults, options)
91   -
92   - if ( $.isFunction(options.url) ) {
93   - options.url = options.url()
94   - }
95   -
96   - options.context = $container
97   -
98   - options.success = function(data){
99   - if ( options.fragment ) {
100   - // If they specified a fragment, look for it in the response
101   - // and pull it out.
102   - var $fragment = $(data).find(options.fragment)
103   - if ( $fragment.length )
104   - data = $fragment.children()
105   - else
106   - return window.location = options.url
107   - } else {
108   - // If we got no data or an entire web page, go directly
109   - // to the page and let normal error handling happen.
110   - if ( !$.trim(data) || /<html/i.test(data) )
111   - return window.location = options.url
112   - }
113   -
114   - // Make it happen.
115   - this.html(data)
116   -
117   - // If there's a <title> tag in the response, use it as
118   - // the page's title.
119   - var oldTitle = document.title,
120   - title = $.trim( this.find('title').remove().text() )
121   - if ( title ) document.title = title
122   -
123   - // No <title>? Fragment? Look for data-title and title attributes.
124   - if ( !title && options.fragment ) {
125   - title = $fragment.attr('title') || $fragment.data('title')
126   - }
127   -
128   - var state = {
129   - pjax: options.container,
130   - fragment: options.fragment,
131   - timeout: options.timeout
132   - }
133   -
134   - // If there are extra params, save the complete URL in the state object
135   - var query = $.param(options.data)
136   - if ( query != "_pjax=true" )
137   - state.url = options.url + (/\?/.test(options.url) ? "&" : "?") + query
138   -
139   - if ( options.replace ) {
140   - window.history.replaceState(state, document.title, options.url)
141   - } else if ( options.push ) {
142   - // this extra replaceState before first push ensures good back
143   - // button behavior
144   - if ( !pjax.active ) {
145   - window.history.replaceState($.extend({}, state, {url:null}), oldTitle)
146   - pjax.active = true
147   - }
148   -
149   - window.history.pushState(state, document.title, options.url)
150   - }
151   -
152   - // Google Analytics support
153   - if ( (options.replace || options.push) && window._gaq )
154   - _gaq.push(['_trackPageview'])
155   -
156   - // If the URL has a hash in it, make sure the browser
157   - // knows to navigate to the hash.
158   - var hash = window.location.hash.toString()
159   - if ( hash !== '' ) {
160   - window.location.href = hash
161   - }
162   -
163   - // Invoke their success handler if they gave us one.
164   - success.apply(this, arguments)
165   - }
166   -
167   - // Cancel the current request if we're already pjaxing
168   - var xhr = pjax.xhr
169   - if ( xhr && xhr.readyState < 4) {
170   - xhr.onreadystatechange = $.noop
171   - xhr.abort()
172   - }
173   -
174   - pjax.options = options
175   - pjax.xhr = $.ajax(options)
176   - $(document).trigger('pjax', [pjax.xhr, options])
177   -
178   - return pjax.xhr
179   -}
180   -
181   -
182   -pjax.defaults = {
183   - timeout: 650,
184   - push: true,
185   - replace: false,
186   - // We want the browser to maintain two separate internal caches: one for
187   - // pjax'd partial page loads and one for normal page loads. Without
188   - // adding this secret parameter, some browsers will often confuse the two.
189   - data: { _pjax: true },
190   - type: 'GET',
191   - dataType: 'html',
192   - beforeSend: function(xhr){
193   - this.trigger('pjax:start', [xhr, pjax.options])
194   - // start.pjax is deprecated
195   - this.trigger('start.pjax', [xhr, pjax.options])
196   - xhr.setRequestHeader('X-PJAX', 'true')
197   - },
198   - error: function(xhr, textStatus, errorThrown){
199   - if ( textStatus !== 'abort' )
200   - window.location = pjax.options.url
201   - },
202   - complete: function(xhr){
203   - this.trigger('pjax:end', [xhr, pjax.options])
204   - // end.pjax is deprecated
205   - this.trigger('end.pjax', [xhr, pjax.options])
206   - }
207   -}
208   -
209   -
210   -// Used to detect initial (useless) popstate.
211   -// If history.state exists, assume browser isn't going to fire initial popstate.
212   -var popped = ('state' in window.history), initialURL = location.href
213   -
214   -
215   -// popstate handler takes care of the back and forward buttons
216   -//
217   -// You probably shouldn't use pjax on pages with other pushState
218   -// stuff yet.
219   -$(window).bind('popstate', function(event){
220   - // Ignore inital popstate that some browsers fire on page load
221   - var initialPop = !popped && location.href == initialURL
222   - popped = true
223   - if ( initialPop ) return
224   -
225   - var state = event.state
226   -
227   - if ( state && state.pjax ) {
228   - var container = state.pjax
229   - if ( $(container+'').length )
230   - $.pjax({
231   - url: state.url || location.href,
232   - fragment: state.fragment,
233   - container: container,
234   - push: false,
235   - timeout: state.timeout
236   - })
237   - else
238   - window.location = location.href
239   - }
240   -})
241   -
242   -
243   -// Add the state property to jQuery's event object so we can use it in
244   -// $(window).bind('popstate')
245   -if ( $.inArray('state', $.event.props) < 0 )
246   - $.event.props.push('state')
247   -
248   -
249   -// Is pjax supported by this browser?
250   -$.support.pjax =
251   - window.history && window.history.pushState && window.history.replaceState
252   - // pushState isn't reliable on iOS yet.
253   - && !navigator.userAgent.match(/(iPod|iPhone|iPad|WebApps\/.+CFNetwork)/)
254   -
255   -
256   -// Fall back to normalcy for older browsers.
257   -if ( !$.support.pjax ) {
258   - $.pjax = function( options ) {
259   - window.location = $.isFunction(options.url) ? options.url() : options.url
260   - }
261   - $.fn.pjax = function() { return this }
262   -}
263   -
264   -})(jQuery);
app/assets/javascripts/rails.js
1 1 /**
2 2 * Unobtrusive scripting adapter for jQuery
3 3 *
4   - * Requires jQuery 1.6.0 or later.
  4 + * Requires jQuery 1.6.0. Not compatible to jquery > 1.9
5 5 * https://github.com/rails/jquery-ujs
6 6  
7 7 * Uploading file using rails.js
... ... @@ -131,11 +131,11 @@
131 131 method = element.data('method');
132 132 url = element.data('url');
133 133 data = element.serialize();
134   - if (element.data('params')) data = data + "&" + element.data('params');
  134 + if (element.data('params')) data = data + "&" + element.data('params');
135 135 } else {
136 136 method = element.data('method');
137 137 url = element.attr('href');
138   - data = element.data('params') || null;
  138 + data = element.data('params') || null;
139 139 }
140 140  
141 141 options = {
... ...
app/assets/stylesheets/errbit.css
... ... @@ -304,7 +304,7 @@ form label.inline { display: inline; }
304 304 form .checkbox label { display: inline; }
305 305 form .required label { padding-right: 20px; background: transparent url(images/icons/required.png) right 50% no-repeat; }
306 306 form .field_with_errors label { color: #900; }
307   -form input[type=text], form input[type=password] {
  307 +form input[type=text], form input[type=password], form input[type=email] {
308 308 width: 96%; padding: 0.8em;
309 309 font-size: 1em;
310 310 color: #787878; border: 1px solid #C6C6C6;
... ... @@ -316,7 +316,7 @@ form textarea {
316 316 }
317 317 form textarea.short { height: 8em; }
318 318 form textarea.supershort { height: 4em; }
319   -form input[type=text]:focus, form input[type=password]:focus, form textarea:focus {
  319 +form input[type=text]:focus, form input[type=password]:focus, form input[type=email]:focus, form textarea:focus {
320 320 box-shadow: 0px 0px 4px #69C;
321 321 -moz-box-shadow: 0px 0px 4px #69C;
322 322 -webkit-box-shadow: 0px 0px 4px #69C
... ... @@ -654,7 +654,6 @@ table.errs td.message a {
654 654 overflow: hidden;
655 655 text-overflow: ellipsis;
656 656 -o-text-overflow: ellipsis;
657   - white-space: nowrap;
658 657 /* ------ */
659 658 }
660 659 table.errs td.message em {
... ... @@ -714,7 +713,9 @@ table.deploys td.when {
714 713 .notice-pagination-loader {
715 714 visibility: hidden;
716 715 float: left;
717   - margin-right: 2em;
  716 + width: 16px;
  717 + height: 16px;
  718 + margin-right: 1em;
718 719 }
719 720 .notice-pagination-loader img {
720 721 vertical-align: middle
... ...
app/controllers/api/v1/notices_controller.rb
1 1 class Api::V1::NoticesController < ApplicationController
2 2 respond_to :json, :xml
3   -
  3 +
4 4 def index
5 5 query = {}
6 6 fields = %w{created_at message error_class}
7   -
  7 +
8 8 if params.key?(:start_date) && params.key?(:end_date)
9 9 start_date = Time.parse(params[:start_date]).utc
10 10 end_date = Time.parse(params[:end_date]).utc
11 11 query = {:created_at => {"$lte" => end_date, "$gte" => start_date}}
12 12 end
13   -
14   - results = benchmark("[api/v1/notices_controller] query time") { Mongoid.master["notices"].find(query, :fields => fields).to_a }
15   -
  13 +
  14 + results = benchmark("[api/v1/notices_controller] query time") do
  15 + Notice.where(query).with(:consistency => :strong).only(fields).to_a
  16 + end
  17 +
16 18 respond_to do |format|
17 19 format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
18 20 format.json { render :json => Yajl.dump(results) }
19 21 format.xml { render :xml => results }
20 22 end
21 23 end
22   -
  24 +
23 25 end
... ...
app/controllers/api/v1/problems_controller.rb
1 1 class Api::V1::ProblemsController < ApplicationController
2 2 respond_to :json, :xml
3   -
  3 +
4 4 def index
5 5 query = {}
6 6 fields = %w{app_id app_name environment message where first_notice_at last_notice_at resolved resolved_at notices_count}
7   -
  7 +
8 8 if params.key?(:start_date) && params.key?(:end_date)
9 9 start_date = Time.parse(params[:start_date]).utc
10 10 end_date = Time.parse(params[:end_date]).utc
11 11 query = {:first_notice_at=>{"$lte"=>end_date}, "$or"=>[{:resolved_at=>nil}, {:resolved_at=>{"$gte"=>start_date}}]}
12 12 end
13   -
14   - results = benchmark("[api/v1/problems_controller] query time") { Mongoid.master["problems"].find(query, :fields => fields).to_a }
15   -
  13 +
  14 + results = benchmark("[api/v1/problems_controller] query time") do
  15 + Problem.where(query).with(:consistency => :strong).only(fields).to_a
  16 + end
  17 +
16 18 respond_to do |format|
17 19 format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
18 20 format.json { render :json => Yajl.dump(results) }
19 21 format.xml { render :xml => results }
20 22 end
21 23 end
22   -
  24 +
23 25 end
... ...
app/controllers/api/v1/stats_controller.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +class Api::V1::StatsController < ApplicationController
  2 + respond_to :json, :xml
  3 +
  4 + # The stats API only requires an api_key for the given app.
  5 + skip_before_filter :authenticate_user!
  6 + before_filter :require_api_key_or_authenticate_user!
  7 +
  8 + def app
  9 + if problem = @app.problems.order_by(:last_notice_at.desc).first
  10 + @last_error_time = problem.last_notice_at
  11 + end
  12 +
  13 + stats = {
  14 + :name => @app.name,
  15 + :last_error_time => @last_error_time,
  16 + :unresolved_errors => @app.unresolved_count
  17 + }
  18 +
  19 + respond_to do |format|
  20 + format.html { render :json => Yajl.dump(stats) } # render JSON if no extension specified on path
  21 + format.json { render :json => Yajl.dump(stats) }
  22 + format.xml { render :xml => stats }
  23 + end
  24 + end
  25 +
  26 +
  27 + protected
  28 +
  29 + def require_api_key_or_authenticate_user!
  30 + if params[:api_key].present?
  31 + if @app = App.where(:api_key => params[:api_key]).first
  32 + return true
  33 + end
  34 + end
  35 +
  36 + authenticate_user!
  37 + end
  38 +
  39 +end
  40 +
  41 +
... ...
app/controllers/application_controller.rb
... ... @@ -13,12 +13,28 @@ class ApplicationController &lt; ActionController::Base
13 13  
14 14 rescue_from ActionController::RedirectBackError, :with => :redirect_to_root
15 15  
  16 + class StrongParametersWithEagerAttributesStrategy < DecentExposure::StrongParametersStrategy
  17 + def attributes
  18 + super
  19 + @attributes ||= params[inflector.param_key] || {}
  20 + end
  21 + end
  22 +
  23 + decent_configuration do
  24 + strategy StrongParametersWithEagerAttributesStrategy
  25 + end
16 26  
17 27 protected
18 28  
19 29  
  30 + ##
  31 + # Check if the current_user is admin or not and redirect to root url if not
  32 + #
20 33 def require_admin!
21   - redirect_to_root unless user_signed_in? && current_user.admin?
  34 + unless user_signed_in? && current_user.admin?
  35 + flash[:error] = "Sorry, you don't have permission to do that"
  36 + redirect_to_root
  37 + end
22 38 end
23 39  
24 40 def redirect_to_root
... ... @@ -30,4 +46,3 @@ protected
30 46 end
31 47  
32 48 end
33   -
... ...
app/controllers/apps_controller.rb
1   -class AppsController < InheritedResources::Base
  1 +class AppsController < ApplicationController
  2 +
  3 + include ProblemsSearcher
  4 +
2 5 before_filter :require_admin!, :except => [:index, :show]
3 6 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update]
  7 + before_filter :parse_notice_at_notices_or_set_default, :only => [:create, :update]
4 8 respond_to :html
5 9  
6   - def show
7   - respond_to do |format|
8   - format.html do
9   - @all_errs = !!params[:all_errs]
  10 + expose(:app_scope) {
  11 + (current_user.admin? ? App : current_user.apps)
  12 + }
  13 +
  14 + expose(:apps) {
  15 + app_scope.all.sort
  16 + }
  17 +
  18 + expose(:app, :ancestor => :app_scope)
  19 +
  20 + expose(:all_errs) {
  21 + !!params[:all_errs]
  22 + }
  23 + expose(:problems) {
  24 + if request.format == :atom
  25 + app.problems.unresolved.ordered
  26 + else
  27 + pr = app.problems
  28 + pr = pr.unresolved unless all_errs
  29 + pr.in_env(
  30 + params[:environment]
  31 + ).ordered_by(params_sort, params_order).page(params[:page]).per(current_user.per_page)
  32 + end
  33 + }
10 34  
11   - @sort = params[:sort]
12   - @order = params[:order]
13   - @sort = "last_notice_at" unless %w{message app last_deploy_at last_notice_at count}.member?(@sort)
14   - @order = "desc" unless %w{asc desc}.member?(@order)
  35 + expose(:deploys) {
  36 + app.deploys.order_by(:created_at.desc).limit(5)
  37 + }
15 38  
16   - @problems = resource.problems
17   - @problems = @problems.unresolved unless @all_errs
18   - @problems = @problems.in_env(params[:environment]).ordered_by(@sort, @order).page(params[:page]).per(current_user.per_page)
  39 + def index; end
  40 + def show
  41 + app
  42 + end
19 43  
20   - @selected_problems = params[:problems] || []
21   - @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
22   - end
23   - format.atom do
24   - @problems = resource.problems.unresolved.ordered
25   - end
26   - end
  44 + def new
  45 + plug_params(app)
27 46 end
28 47  
29 48 def create
30   - @app = App.new(params[:app])
31 49 initialize_subclassed_issue_tracker
32 50 initialize_subclassed_notification_service
33   - create!
  51 + if app.save
  52 + redirect_to app_url(app), :flash => { :success => I18n.t('controllers.apps.flash.create.success') }
  53 + else
  54 + flash[:error] = I18n.t('controllers.apps.flash.create.error')
  55 + render :new
  56 + end
34 57 end
35 58  
36 59 def update
37   - @app = resource
38 60 initialize_subclassed_issue_tracker
39 61 initialize_subclassed_notification_service
40   - update!
  62 + if app.save
  63 + redirect_to app_url(app), :flash => { :success => I18n.t('controllers.apps.flash.update.success') }
  64 + else
  65 + flash[:error] = I18n.t('controllers.apps.flash.update.error')
  66 + render :edit
  67 + end
41 68 end
42 69  
43   - def new
44   - plug_params(build_resource)
45   - new!
  70 + def edit
  71 + plug_params(app)
46 72 end
47 73  
48   - def edit
49   - plug_params(resource)
50   - edit!
  74 + def destroy
  75 + if app.destroy
  76 + redirect_to apps_url, :flash => { :success => I18n.t('controllers.apps.flash.destroy.success') }
  77 + else
  78 + flash[:error] = I18n.t('controllers.apps.flash.destroy.error')
  79 + render :show
  80 + end
51 81 end
52 82  
53 83 protected
54   - def collection
55   - @apps ||= end_of_association_chain.all.sort
56   - end
57 84  
58 85 def initialize_subclassed_issue_tracker
59 86 # set the app's issue tracker
60 87 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type]
61 88 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type)
62   - @app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
  89 + app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
63 90 end
64 91 end
65 92 end
... ... @@ -68,21 +95,11 @@ class AppsController &lt; InheritedResources::Base
68 95 # set the app's notification service
69 96 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
70 97 if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
71   - @app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
  98 + app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
72 99 end
73 100 end
74 101 end
75 102  
76   - def begin_of_association_chain
77   - # Filter the @apps collection to apps watched by the current user, unless user is an admin.
78   - # If user is an admin, then no filter is applied, and all apps are shown.
79   - current_user unless current_user.admin?
80   - end
81   -
82   - def interpolation_options
83   - {:app_name => resource.name}
84   - end
85   -
86 103 def plug_params app
87 104 app.watchers.build if app.watchers.none?
88 105 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured?
... ... @@ -105,5 +122,20 @@ class AppsController &lt; InheritedResources::Base
105 122 end
106 123 end
107 124 end
  125 +
  126 + def parse_notice_at_notices_or_set_default
  127 + if params[:app][:notification_service_attributes] && val = params[:app][:notification_service_attributes][:notify_at_notices]
  128 + # Sanitize negative values, split on comma,
  129 + # strip, parse as integer, remove all '0's.
  130 + # If empty, set as default and show an error message.
  131 + notify_at_notices = val.gsub(/-\d+/,"").split(",").map{|v| v.strip.to_i }
  132 + if notify_at_notices.any?
  133 + params[:app][:notification_service_attributes][:notify_at_notices] = notify_at_notices
  134 + else
  135 + default_array = params[:app][:notification_service_attributes][:notify_at_notices] = Errbit::Config.notify_at_notices
  136 + flash[:error] = "Couldn't parse your notification frequency. Value was reset to default (#{default_array.join(', ')})."
  137 + end
  138 + end
  139 + end
108 140 end
109 141  
... ...
app/controllers/notices_controller.rb
1 1 class NoticesController < ApplicationController
2   - respond_to :xml
  2 +
  3 + class ParamsError < StandardError; end
3 4  
4 5 skip_before_filter :authenticate_user!, :only => :create
5 6  
  7 + rescue_from ParamsError, :with => :bad_params
  8 +
6 9 def create
7 10 # params[:data] if the notice came from a GET request, raw_post if it came via POST
8   - notice = App.report_error!(params[:data] || request.raw_post)
9   - api_xml = notice.to_xml(:only => false, :methods => [:id]) do |xml|
10   - xml.url locate_url(notice.id, :host => Errbit::Config.host)
  11 + report = ErrorReport.new(notice_params)
  12 +
  13 + if report.valid?
  14 + report.generate_notice!
  15 + api_xml = report.notice.to_xml(:only => false, :methods => [:id]) do |xml|
  16 + xml.url locate_url(report.notice.id, :host => Errbit::Config.host)
  17 + end
  18 + render :xml => api_xml
  19 + else
  20 + render :text => "Your API key is unknown", :status => 422
11 21 end
12   - render :xml => api_xml
13 22 end
14 23  
15 24 # Redirects a notice to the problem page. Useful when using User Information at Airbrake gem.
... ... @@ -17,4 +26,20 @@ class NoticesController &lt; ApplicationController
17 26 problem = Notice.find(params[:id]).problem
18 27 redirect_to app_problem_path(problem.app, problem)
19 28 end
  29 +
  30 + private
  31 +
  32 + def notice_params
  33 + return @notice_params if @notice_params
  34 + @notice_params = params[:data] || request.raw_post
  35 + if @notice_params.blank?
  36 + raise ParamsError.new('Need a data params in GET or raw post data')
  37 + end
  38 + @notice_params
  39 + end
  40 +
  41 + def bad_params(exception)
  42 + render :text => exception.message, :status => :bad_request
  43 + end
  44 +
20 45 end
... ...
app/controllers/problems_controller.rb
  1 +##
  2 +# Manage problems
  3 +#
  4 +# List of actions available :
  5 +# MEMBER => :show, :edit, :update, :create, :destroy, :resolve, :unresolve, :create_issue, :unlink_issue
  6 +# COLLECTION => :index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several, :search
1 7 class ProblemsController < ApplicationController
2   - before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
3   - before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
4   - before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
5   - before_filter :set_sorting_params, :only => [:index, :all]
6   - before_filter :set_tracker_params, :only => [:create_issue]
7 8  
8   - def index
9   - app_scope = current_user.admin? ? App.all : current_user.apps
10 9  
11   - @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered_by(@sort, @order)
12   - @selected_problems = params[:problems] || []
13   - respond_to do |format|
14   - format.html do
15   - @problems = @problems.page(params[:page]).per(current_user.per_page)
16   - end
17   - format.atom
  10 + include ProblemsSearcher
  11 +
  12 + before_filter :need_selected_problem, :only => [
  13 + :resolve_several, :unresolve_several, :unmerge_several
  14 + ]
  15 +
  16 + expose(:app) {
  17 + if current_user.admin?
  18 + App.find(params[:app_id])
  19 + else
  20 + current_user.apps.find(params[:app_id])
18 21 end
19   - end
  22 + }
20 23  
21   - def all
22   - app_scope = current_user.admin? ? App.all : current_user.apps
23   - @problems = Problem.for_apps(app_scope).ordered_by(@sort, @order).page(params[:page]).per(current_user.per_page)
24   - @selected_problems = params[:problems] || []
25   - end
  24 + expose(:problem) {
  25 + app.problems.find(params[:id])
  26 + }
  27 +
  28 +
  29 + expose(:all_errs) {
  30 + params[:all_errs]
  31 + }
  32 +
  33 + expose(:app_scope) {
  34 + apps = current_user.admin? ? App.all : current_user.apps
  35 + params[:app_id] ? apps.where(:_id => params[:app_id]) : apps
  36 + }
  37 +
  38 + expose(:params_environement) {
  39 + params[:environment]
  40 + }
  41 +
  42 + expose(:problems) {
  43 + pro = Problem.for_apps(
  44 + app_scope
  45 + ).in_env(
  46 + params_environement
  47 + ).all_else_unresolved(all_errs).ordered_by(params_sort, params_order)
  48 +
  49 + if request.format == :html
  50 + pro.page(params[:page]).per(current_user.per_page)
  51 + else
  52 + pro
  53 + end
  54 + }
  55 +
  56 + def index; end
26 57  
27 58 def show
28   - @notices = @problem.notices.reverse_ordered.page(params[:notice]).per(1)
  59 + @notices = problem.notices.reverse_ordered.page(params[:notice]).per(1)
29 60 @notice = @notices.first
30 61 @comment = Comment.new
31   - if request.headers['X-PJAX']
32   - params["_pjax"] = nil
33   - render :layout => false
34   - end
35 62 end
36 63  
37 64 def create_issue
38   - issue_creation = IssueCreation.new(@problem, current_user, params[:tracker])
  65 + IssueTracker.update_url_options(request)
  66 + issue_creation = IssueCreation.new(problem, current_user, params[:tracker])
39 67  
40 68 unless issue_creation.execute
41   - flash[:error] = issue_creation.errors[:base].first
  69 + flash[:error] = issue_creation.errors.full_messages.join(', ')
42 70 end
43 71  
44   - redirect_to app_problem_path(@app, @problem)
  72 + redirect_to app_problem_path(app, problem)
45 73 end
46 74  
47 75 def unlink_issue
48   - @problem.update_attribute :issue_link, nil
49   - redirect_to app_problem_path(@app, @problem)
  76 + problem.update_attribute :issue_link, nil
  77 + redirect_to app_problem_path(app, problem)
50 78 end
51 79  
52 80 def resolve
53   - @problem.resolve!
  81 + problem.resolve!
54 82 flash[:success] = 'Great news everyone! The err has been resolved.'
55 83 redirect_to :back
56 84 rescue ActionController::RedirectBackError
57   - redirect_to app_path(@app)
  85 + redirect_to app_path(app)
58 86 end
59 87  
60 88 def resolve_several
61   - @selected_problems.each(&:resolve!)
62   - flash[:success] = "Great news everyone! #{I18n.t(:n_errs_have, :count => @selected_problems.count)} been resolved."
  89 + selected_problems.each(&:resolve!)
  90 + flash[:success] = "Great news everyone! #{I18n.t(:n_errs_have, :count => selected_problems.count)} been resolved."
63 91 redirect_to :back
64 92 end
65 93  
66 94 def unresolve_several
67   - @selected_problems.each(&:unresolve!)
68   - flash[:success] = "#{I18n.t(:n_errs_have, :count => @selected_problems.count)} been unresolved."
  95 + selected_problems.each(&:unresolve!)
  96 + flash[:success] = "#{I18n.t(:n_errs_have, :count => selected_problems.count)} been unresolved."
69 97 redirect_to :back
70 98 end
71 99  
  100 + ##
  101 + # Action to merge several Problem in One problem
  102 + #
  103 + # @param [ Array<String> ] :problems the list of problem ids
  104 + #
72 105 def merge_several
73   - if @selected_problems.length < 2
74   - flash[:notice] = "You must select at least two errors to merge"
  106 + if selected_problems.length < 2
  107 + flash[:notice] = I18n.t('controllers.problems.flash.need_two_errors_merge')
75 108 else
76   - @merged_problem = Problem.merge!(@selected_problems)
77   - flash[:notice] = "#{@selected_problems.count} errors have been merged."
  109 + ProblemMerge.new(selected_problems).merge
  110 + flash[:notice] = I18n.t('controllers.problems.flash.merge_several.success', :nb => selected_problems.count)
78 111 end
79 112 redirect_to :back
80 113 end
81 114  
82 115 def unmerge_several
83   - all = @selected_problems.map(&:unmerge!).flatten
  116 + all = selected_problems.map(&:unmerge!).flatten
84 117 flash[:success] = "#{I18n.t(:n_errs_have, :count => all.length)} been unmerged."
85 118 redirect_to :back
86 119 end
87 120  
88 121 def destroy_several
89   - nb_problem_destroy = ProblemDestroy.execute(@selected_problems)
  122 + nb_problem_destroy = ProblemDestroy.execute(selected_problems)
90 123 flash[:notice] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted."
91 124 redirect_to :back
92 125 end
93 126  
94   - protected
95   - def find_app
96   - @app = App.find(params[:app_id])
97   -
98   - # Mongoid Bug: could not chain: current_user.apps.find_by_id!
99   - # apparently finding by 'watchers.email' and 'id' is broken
100   - raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
101   - end
102   -
103   - def find_problem
104   - @problem = @app.problems.find(params[:id])
105   - end
106   -
107   - def set_tracker_params
108   - IssueTracker.default_url_options[:host] = request.host
109   - IssueTracker.default_url_options[:port] = request.port
110   - IssueTracker.default_url_options[:protocol] = request.scheme
  127 + def search
  128 + ps = Problem.search(params[:search]).for_apps(app_scope).in_env(params[:environment]).all_else_unresolved(params[:all_errs]).ordered_by(params_sort, params_order)
  129 + selected_problems = params[:problems] || []
  130 + self.problems = ps.page(params[:page]).per(2)
  131 + respond_to do |format|
  132 + format.html { render :index }
  133 + format.js
111 134 end
  135 + end
112 136  
113   - def find_selected_problems
114   - err_ids = (params[:problems] || []).compact
115   - if err_ids.empty?
116   - flash[:notice] = "You have not selected any errors"
117   - redirect_to :back
118   - else
119   - @selected_problems = Array(Problem.find(err_ids))
120   - end
121   - end
  137 + protected
122 138  
123   - def set_sorting_params
124   - @sort = params[:sort]
125   - @sort = "last_notice_at" unless %w{app message last_notice_at last_deploy_at count}.member?(@sort)
126   - @order = params[:order] || "desc"
  139 + ##
  140 + # Redirect :back if no errors selected
  141 + #
  142 + def need_selected_problem
  143 + if err_ids.empty?
  144 + flash[:notice] = I18n.t('controllers.problems.flash.no_select_problem')
  145 + redirect_to :back
127 146 end
  147 + end
128 148 end
129 149  
... ...
app/controllers/problems_searcher.rb 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +# Include to do a Search
  2 +# TODO: Need to be in a Dedicated Object ProblemsSearch with params like input
  3 +#
  4 +module ProblemsSearcher
  5 + extend ActiveSupport::Concern
  6 +
  7 + included do
  8 +
  9 + expose(:params_sort) {
  10 + unless %w{app message last_notice_at last_deploy_at count}.member?(params[:sort])
  11 + "last_notice_at"
  12 + else
  13 + params[:sort]
  14 + end
  15 + }
  16 +
  17 + expose(:params_order){
  18 + unless %w{asc desc}.member?(params[:order])
  19 + 'desc'
  20 + else
  21 + params[:order]
  22 + end
  23 + }
  24 +
  25 + expose(:selected_problems) {
  26 + Array(Problem.find(err_ids))
  27 + }
  28 +
  29 + expose(:err_ids) {
  30 + (params[:problems] || []).compact
  31 + }
  32 +
  33 + end
  34 +end
... ...
app/controllers/users_controller.rb
... ... @@ -2,71 +2,76 @@ class UsersController &lt; ApplicationController
2 2 respond_to :html
3 3  
4 4 before_filter :require_admin!, :except => [:edit, :update]
5   - before_filter :find_user, :only => [:show, :edit, :update, :destroy, :unlink_github]
6 5 before_filter :require_user_edit_priviledges, :only => [:edit, :update]
7 6  
8   - def index
9   - @users = User.all.page(params[:page]).per(current_user.per_page)
10   - end
  7 + expose(:user, :attributes => :user_params)
  8 + expose(:users) {
  9 + User.all.page(params[:page]).per(current_user.per_page)
  10 + }
11 11  
12   - def new
13   - @user = User.new
14   - end
  12 + def index; end
  13 + def new; end
  14 + def show; end
15 15  
16 16 def create
17   - @user = User.new(params[:user])
18   -
19   - # Set protected attributes
20   - @user.admin = params[:user].try(:[], :admin) if current_user.admin?
21   -
22   - if @user.save
23   - flash[:success] = "#{@user.name} is now part of the team. Be sure to add them as a project watcher."
24   - redirect_to user_path(@user)
  17 + if user.save
  18 + flash[:success] = "#{user.name} is now part of the team. Be sure to add them as a project watcher."
  19 + redirect_to user_path(user)
25 20 else
26 21 render :new
27 22 end
28 23 end
29 24  
30 25 def update
31   - # Devise Hack
32   - if params[:user][:password].blank? && params[:user][:password_confirmation].blank?
33   - params[:user].delete(:password)
34   - params[:user].delete(:password_confirmation)
35   - end
36   -
37   - # Set protected attributes
38   - @user.admin = params[:user][:admin] if current_user.admin?
39   -
40   - if @user.update_attributes(params[:user])
41   - flash[:success] = "#{@user.name}'s information was successfully updated"
42   - redirect_to user_path(@user)
  26 + if user.update_attributes(user_params)
  27 + flash[:success] = I18n.t('controllers.users.flash.update.success', :name => user.name)
  28 + redirect_to user_path(user)
43 29 else
44 30 render :edit
45 31 end
46 32 end
47 33  
  34 + ##
  35 + # Destroy the user pass in args
  36 + #
  37 + # @param [ String ] id the id of user we want delete
  38 + #
48 39 def destroy
49   - @user.destroy
50   -
51   - flash[:success] = "That's sad. #{@user.name} is no longer part of your team."
  40 + if user == current_user
  41 + flash[:error] = I18n.t('controllers.users.flash.destroy.error')
  42 + else
  43 + UserDestroy.new(user).destroy
  44 + flash[:success] = I18n.t('controllers.users.flash.destroy.success', :name => user.name)
  45 + end
52 46 redirect_to users_path
53 47 end
54 48  
55 49 def unlink_github
56   - @user.update_attributes :github_login => nil, :github_oauth_token => nil
57   - redirect_to user_path(@user)
  50 + user.update_attributes :github_login => nil, :github_oauth_token => nil
  51 + redirect_to user_path(user)
58 52 end
59 53  
60 54 protected
61 55  
62   - def find_user
63   - @user = User.find(params[:id])
64   - end
65   -
66 56 def require_user_edit_priviledges
67   - can_edit = current_user == @user || current_user.admin?
  57 + can_edit = current_user == user || current_user.admin?
68 58 redirect_to(root_path) and return(false) unless can_edit
69 59 end
70 60  
  61 + def user_params
  62 + @user_params ||= params[:user] ? params.require(:user).permit(*user_permit_params) : {}
  63 + end
  64 +
  65 + def user_permit_params
  66 + @user_permit_params ||= [:name,:username, :email, :github_login, :per_page, :time_zone]
  67 + @user_permit_params << :admin if current_user.admin? && current_user.id != params[:id]
  68 + @user_permit_params |= [:password, :password_confirmation] if user_password_params.values.all?{|pa| !pa.blank? }
  69 + @user_permit_params
  70 + end
  71 +
  72 + def user_password_params
  73 + @user_password_params ||= params[:user] ? params.require(:user).permit(:password, :password_confirmation) : {}
  74 + end
  75 +
71 76 end
72 77  
... ...
app/helpers/application_helper.rb
... ... @@ -8,11 +8,11 @@ module ApplicationHelper
8 8 notices.each_with_index do |notice,idx|
9 9 cal.event do |event|
10 10 event.summary = "#{idx+1} #{notice.message.to_s}"
11   - event.description = notice.request['url']
  11 + event.description = notice.url if notice.url
12 12 event.dtstart = notice.created_at.utc
13 13 event.dtend = notice.created_at.utc + 60.minutes
14 14 event.organizer = notice.server_environment && notice.server_environment["hostname"]
15   - event.location = notice.server_environment && notice.server_environment["project-root"]
  15 + event.location = notice.project_root
16 16 event.url = app_problem_url(:app_id => notice.problem.app.id, :id => notice.problem)
17 17 end
18 18 end
... ... @@ -57,7 +57,7 @@ module ApplicationHelper
57 57 total = (options[:total] || total_from_tallies(tallies))
58 58 percent = 100.0 / total.to_f
59 59 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \
60   - .sort {|a, b| a[0] <=> b[0]}
  60 + .sort {|a, b| b[0] <=> a[0]}
61 61 render "problems/tally_table", :rows => rows
62 62 end
63 63  
... ...
app/helpers/apps_helper.rb
... ... @@ -4,7 +4,7 @@ module AppsHelper
4 4 html = link_to('copy settings from another app', '#',
5 5 :class => 'button copy_config')
6 6 html << select("duplicate", "app",
7   - App.all.reject{|a| a == @app }.
  7 + App.all.asc(:name).reject{|a| a == @app }.
8 8 collect{|p| [ p.name, p.id ] }, {:include_blank => "[choose app]"},
9 9 {:class => "choose_other_app", :style => "display: none;"})
10 10 return html
... ... @@ -41,7 +41,7 @@ module AppsHelper
41 41 def detect_any_apps_with_attributes
42 42 @any_github_repos = @any_issue_trackers = @any_deploys = @any_bitbucket_repos = @any_notification_services = false
43 43  
44   - @apps.each do |app|
  44 + apps.each do |app|
45 45 @any_github_repos ||= app.github_repo?
46 46 @any_bitbucket_repos ||= app.bitbucket_repo?
47 47 @any_issue_trackers ||= app.issue_tracker_configured?
... ...
app/helpers/backtrace_line_helper.rb
1 1 module BacktraceLineHelper
2 2 def link_to_source_file(line, &block)
3 3 text = capture_haml(&block)
4   - line.in_app? ? link_to_in_app_source_file(line, text) : link_to_external_source_file(text)
  4 + link_to_in_app_source_file(line, text) || link_to_external_source_file(text)
5 5 end
6 6  
7 7 private
8 8 def link_to_in_app_source_file(line, text)
9   - link_to_repo_source_file(line, text) || link_to_issue_tracker_file(line, text)
  9 + return unless line.in_app?
  10 + if line.file_name =~ /\.js$/
  11 + link_to_hosted_javascript(line, text)
  12 + else
  13 + link_to_repo_source_file(line, text) ||
  14 + link_to_issue_tracker_file(line, text)
  15 + end
10 16 end
11 17  
12 18 def link_to_repo_source_file(line, text)
13 19 link_to_github(line, text) || link_to_bitbucket(line, text)
14 20 end
15 21  
  22 + def link_to_hosted_javascript(line, text)
  23 + if line.app.asset_host?
  24 + link_to(text, "#{line.app.asset_host}/#{line.file_relative}", :target => '_blank')
  25 + end
  26 + end
  27 +
16 28 def link_to_external_source_file(text)
17 29 text
18 30 end
... ... @@ -31,7 +43,7 @@ module BacktraceLineHelper
31 43  
32 44 def link_to_issue_tracker_file(line, text = nil)
33 45 return unless line.app.issue_tracker && line.app.issue_tracker.respond_to?(:url_to_file)
34   - href = line.app.issue_tracker.url_to_file(line.file, line.number)
  46 + href = line.app.issue_tracker.url_to_file(line.file_relative, line.number)
35 47 link_to(text || line.file_name, href, :target => '_blank')
36 48 end
37 49  
... ...
app/helpers/problems_helper.rb
... ... @@ -11,17 +11,22 @@ module ProblemsHelper
11 11 end
12 12  
13 13 def gravatar_tag(email, options = {})
  14 + return nil unless email.present?
  15 +
14 16 image_tag gravatar_url(email, options), :alt => email, :class => 'gravatar'
15 17 end
16 18  
17 19 def gravatar_url(email, options = {})
  20 + return nil unless email.present?
  21 +
18 22 default_options = {
19 23 :d => Errbit::Config.gravatar_default,
20 24 }
21 25 options.reverse_merge! default_options
22 26 params = options.extract!(:s, :d).delete_if { |k, v| v.blank? }
23 27 email_hash = Digest::MD5.hexdigest(email)
24   - "http://www.gravatar.com/avatar/#{email_hash}?#{params.to_query}"
  28 + url = request.ssl? ? "https://secure.gravatar.com" : "http://www.gravatar.com"
  29 + "#{url}/avatar/#{email_hash}?#{params.to_query}"
25 30 end
26 31 end
27 32  
... ...
app/helpers/sort_helper.rb
1 1 # encoding: utf-8
2 2 module SortHelper
3   -
  3 +
4 4 def link_for_sort(name, field=nil)
5 5 field ||= name.underscore
6   - current = (@sort == field)
7   - order = (current && (@order == "asc")) ? "desc" : "asc"
  6 + current = (params_sort == field)
  7 + order = (current && (params_order == "asc")) ? "desc" : "asc"
8 8 url = request.path + "?sort=#{field}&order=#{order}"
9 9 options = {}
10 10 options.merge!(:class => "current #{order}") if current
11 11 link_to(name, url, options)
12 12 end
13   -
  13 +
14 14 end
... ...
app/interactors/issue_creation.rb
... ... @@ -41,15 +41,11 @@ class IssueCreation
41 41 end
42 42  
43 43 def execute
44   - if tracker
45   - begin
46   - tracker.create_issue problem, user
47   - rescue => ex
48   - Rails.logger.error "Error during issue creation: " << ex.message
49   - errors.add :base, "There was an error during issue creation: #{ex.message}"
50   - end
51   - end
52   -
  44 + tracker.create_issue problem, user if tracker
53 45 errors.empty?
  46 + rescue => ex
  47 + Rails.logger.error "Error during issue creation: " << ex.message
  48 + errors.add :base, "There was an error during issue creation: #{ex.message}"
  49 + false
54 50 end
55 51 end
... ...
app/interactors/problem_destroy.rb
... ... @@ -37,12 +37,12 @@ class ProblemDestroy
37 37 end
38 38  
39 39 def delete_errs
40   - Err.collection.remove(:_id => { '$in' => errs_id })
41   - Notice.collection.remove(:err_id => { '$in' => errs_id })
  40 + Notice.delete_all(:err_id => { '$in' => errs_id })
  41 + Err.delete_all(:_id => { '$in' => errs_id })
42 42 end
43 43  
44 44 def delete_comments
45   - Comment.collection.remove(:_id => { '$in' => comments_id })
  45 + Comment.delete_all(:_id => { '$in' => comments_id })
46 46 end
47 47  
48 48 end
... ...
app/interactors/problem_merge.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +require 'problem_destroy'
  2 +
  3 +class ProblemMerge
  4 + def initialize(*problems)
  5 + problems = problems.flatten.uniq
  6 + @merged_problem = problems[0]
  7 + @child_problems = problems[1..-1]
  8 + raise ArgumentError.new("need almost 2 uniq different problems") if @child_problems.empty?
  9 + end
  10 + attr_reader :merged_problem, :child_problems
  11 +
  12 + def merge
  13 + child_problems.each do |problem|
  14 + merged_problem.errs.concat problem.errs
  15 + merged_problem.comments.concat problem.comments
  16 + problem.reload # deference all associate objet to avoid delete him after
  17 + ProblemDestroy.execute(problem)
  18 + end
  19 + reset_cached_attributes
  20 + merged_problem
  21 + end
  22 +
  23 + private
  24 +
  25 + def reset_cached_attributes
  26 + ProblemUpdaterCache.new(merged_problem).update
  27 + end
  28 +end
... ...
app/interactors/problem_updater_cache.rb 0 → 100644
... ... @@ -0,0 +1,88 @@
  1 +class ProblemUpdaterCache
  2 + def initialize(problem, notice=nil)
  3 + @problem = problem
  4 + @notice = notice
  5 + end
  6 + attr_reader :problem
  7 +
  8 + ##
  9 + # Update cache information about child associate to this problem
  10 + #
  11 + # update the notices count, and some notice informations
  12 + #
  13 + # @return [ Problem ] the problem with this update
  14 + #
  15 + def update
  16 + update_notices_count
  17 + update_notices_cache
  18 + problem
  19 + end
  20 +
  21 + private
  22 +
  23 + def update_notices_count
  24 + if @notice
  25 + problem.inc(:notices_count, 1)
  26 + else
  27 + problem.update_attribute(
  28 + :notices_count, problem.notices.count
  29 + )
  30 + end
  31 + end
  32 +
  33 + ##
  34 + # Update problem statistique from some notice information
  35 + #
  36 + def update_notices_cache
  37 + first_notice = notices.first
  38 + last_notice = notices.last
  39 + notice ||= @notice || first_notice
  40 +
  41 + attrs = {}
  42 + attrs[:first_notice_at] = first_notice.created_at if first_notice
  43 + attrs[:last_notice_at] = last_notice.created_at if last_notice
  44 + attrs.merge!(
  45 + :message => notice.message,
  46 + :where => notice.where,
  47 + :messages => attribute_count(:message, messages),
  48 + :hosts => attribute_count(:host, hosts),
  49 + :user_agents => attribute_count(:user_agent_string, user_agents)
  50 + ) if notice
  51 + problem.update_attributes!(attrs)
  52 + end
  53 +
  54 + def notices
  55 + @notices ||= @notice ? [@notice].sort(&:created_at) : problem.notices.order_by([:created_at, :asc])
  56 + end
  57 +
  58 + def messages
  59 + @notice ? problem.messages : {}
  60 + end
  61 +
  62 + def hosts
  63 + @notice ? problem.hosts : {}
  64 + end
  65 +
  66 + def user_agents
  67 + @notice ? problem.user_agents : {}
  68 + end
  69 +
  70 + private
  71 +
  72 + def attribute_count(value, init)
  73 + init.tap do |counts|
  74 + notices.each do |notice|
  75 + counts[attribute_index(notice.send(value))] ||= {
  76 + 'value' => notice.send(value),
  77 + 'count' => 0
  78 + }
  79 + counts[attribute_index(notice.send(value))]['count'] += 1
  80 + end
  81 + end
  82 + end
  83 +
  84 + def attribute_index(value)
  85 + @attributes_index ||= {}
  86 + @attributes_index[value.to_s] ||= Digest::MD5.hexdigest(value.to_s)
  87 + end
  88 +end
... ...
app/interactors/resolved_problem_clearer.rb 0 → 100644
... ... @@ -0,0 +1,32 @@
  1 +require 'problem_destroy'
  2 +
  3 +class ResolvedProblemClearer
  4 +
  5 + ##
  6 + # Clear all problem already resolved
  7 + #
  8 + def execute
  9 + nb_problem_resolved.tap { |nb|
  10 + if nb > 0
  11 + criteria.each do |problem|
  12 + ProblemDestroy.new(problem).execute
  13 + end
  14 + repair_database
  15 + end
  16 + }
  17 + end
  18 +
  19 + private
  20 +
  21 + def nb_problem_resolved
  22 + @count ||= criteria.count
  23 + end
  24 +
  25 + def criteria
  26 + @criteria = Problem.resolved
  27 + end
  28 +
  29 + def repair_database
  30 + Mongoid.default_session.command :repairDatabase => 1
  31 + end
  32 +end
... ...
app/interactors/user_destroy.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class UserDestroy
  2 + def initialize(user)
  3 + @user = user
  4 + end
  5 +
  6 + def destroy
  7 + @user.destroy
  8 + @user.watchers.each(&:destroy)
  9 + end
  10 +
  11 +end
... ...
app/mailers/mailer.rb
... ... @@ -3,14 +3,20 @@
3 3 require Rails.root.join('config/routes.rb')
4 4  
5 5 class Mailer < ActionMailer::Base
  6 + helper ApplicationHelper
  7 + helper BacktraceLineHelper
  8 +
6 9 default :from => Errbit::Config.email_from
7 10  
8 11 def err_notification(notice)
9 12 @notice = notice
10 13 @app = notice.app
11 14  
  15 + count = @notice.similar_count
  16 + count = count > 1 ? "(#{count}) " : ""
  17 +
12 18 mail :to => @app.notification_recipients,
13   - :subject => "[#{@app.name}][#{@notice.environment_name}] #{@notice.message}"
  19 + :subject => "#{count}[#{@app.name}][#{@notice.environment_name}] #{@notice.message.truncate(50)}"
14 20 end
15 21  
16 22 def deploy_notification(deploy)
... ... @@ -20,5 +26,17 @@ class Mailer &lt; ActionMailer::Base
20 26 mail :to => @app.notification_recipients,
21 27 :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}"
22 28 end
23   -end
24 29  
  30 + def comment_notification(comment)
  31 + @comment = comment
  32 + @user = comment.user
  33 + @problem = comment.err
  34 + @notice = @problem.notices.first
  35 + @app = @problem.app
  36 +
  37 + recipients = @comment.notification_recipients
  38 +
  39 + mail :to => recipients,
  40 + :subject => "#{@user.name} commented on [#{@app.name}][#{@notice.environment_name}] #{@notice.message.truncate(50)}"
  41 + end
  42 +end
... ...
app/models/app.rb
1 1 class App
  2 + include Comparable
2 3 include Mongoid::Document
3 4 include Mongoid::Timestamps
4   - include Comparable
5 5  
6 6 field :name, :type => String
7 7 field :api_key
8 8 field :github_repo
9 9 field :bitbucket_repo
  10 + field :asset_host
10 11 field :repository_branch
11 12 field :resolve_errs_on_deploy, :type => Boolean, :default => false
12 13 field :notify_all_users, :type => Boolean, :default => false
... ... @@ -15,7 +16,12 @@ class App
15 16 field :email_at_notices, :type => Array, :default => Errbit::Config.email_at_notices
16 17  
17 18 # Some legacy apps may have string as key instead of BSON::ObjectID
18   - identity :type => String
  19 + # identity :type => String
  20 + field :_id,
  21 + type: String,
  22 + pre_processed: true,
  23 + default: ->{ Moped::BSON::ObjectId.new.to_s }
  24 +
19 25  
20 26 embeds_many :watchers
21 27 embeds_many :deploys
... ... @@ -41,46 +47,17 @@ class App
41 47 accepts_nested_attributes_for :notification_service, :allow_destroy => true,
42 48 :reject_if => proc { |attrs| !NotificationService.subclasses.map(&:to_s).include?(attrs[:type].to_s) }
43 49  
44   - # Processes a new error report.
45   - #
46   - # Accepts either XML or a hash with the following attributes:
47   - #
48   - # * <tt>:error_class</tt> - the class of error
49   - # * <tt>:message</tt> - the error message
50   - # * <tt>:backtrace</tt> - an array of stack trace lines
51   - #
52   - # * <tt>:request</tt> - a hash of values describing the request
53   - # * <tt>:server_environment</tt> - a hash of values describing the server environment
54   - #
55   - # * <tt>:api_key</tt> - the API key with which the error was reported
56   - # * <tt>:notifier</tt> - information to identify the source of the error report
57   - #
58   - def self.report_error!(*args)
59   - report = ErrorReport.new(*args)
60   - report.generate_notice!
61   - end
62   -
63   -
64   - # Processes a new error report.
65   - #
66   - # Accepts a hash with the following attributes:
  50 + # Acceps a hash with the following attributes:
67 51 #
68   - # * <tt>:error_class</tt> - the class of error
69   - # * <tt>:message</tt> - the error message
70   - # * <tt>:backtrace</tt> - an array of stack trace lines
  52 + # * <tt>:error_class</tt> - the class of error (required to create a new Problem)
  53 + # * <tt>:environment</tt> - the environment the source app was running in (required to create a new Problem)
  54 + # * <tt>:fingerprint</tt> - a unique value identifying the notice
71 55 #
72   - # * <tt>:request</tt> - a hash of values describing the request
73   - # * <tt>:server_environment</tt> - a hash of values describing the server environment
74   - #
75   - # * <tt>:notifier</tt> - information to identify the source of the error report
76   - #
77   - def report_error!(hash)
78   - report = ErrorReport.new(hash.merge(:api_key => api_key))
79   - report.generate_notice!
80   - end
81   -
82 56 def find_or_create_err!(attrs)
83   - Err.where(:fingerprint => attrs[:fingerprint]).first || problems.create!.errs.create!(attrs)
  57 + Err.where(
  58 + :fingerprint => attrs[:fingerprint]
  59 + ).first ||
  60 + problems.create!(attrs.slice(:error_class, :environment)).errs.create!(attrs.slice(:fingerprint, :problem_id))
84 61 end
85 62  
86 63 # Mongoid Bug: find(id) on association proxies returns an Enumerator
... ... @@ -89,7 +66,7 @@ class App
89 66 end
90 67  
91 68 def self.find_by_api_key!(key)
92   - where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key))
  69 + find_by(:api_key => key)
93 70 end
94 71  
95 72 def last_deploy_at
... ... @@ -103,7 +80,7 @@ class App
103 80 end
104 81 alias :notify_on_errs? :notify_on_errs
105 82  
106   - def notifiable?
  83 + def emailable?
107 84 notify_on_errs? && notification_recipients.any?
108 85 end
109 86  
... ... @@ -125,7 +102,7 @@ class App
125 102 end
126 103  
127 104 def github_url_to_file(file)
128   - "#{github_url}/blob/#{repo_branch + file}"
  105 + "#{github_url}/blob/#{repo_branch}/#{file}"
129 106 end
130 107  
131 108 def bitbucket_repo?
... ... @@ -137,7 +114,7 @@ class App
137 114 end
138 115  
139 116 def bitbucket_url_to_file(file)
140   - "#{bitbucket_url}/src/#{repo_branch + file}"
  117 + "#{bitbucket_url}/src/#{repo_branch}/#{file}"
141 118 end
142 119  
143 120  
... ...
app/models/backtrace.rb
... ... @@ -3,7 +3,7 @@ class Backtrace
3 3 include Mongoid::Timestamps
4 4  
5 5 field :fingerprint
6   - index :fingerprint
  6 + index :fingerprint => 1
7 7  
8 8 has_many :notices
9 9 has_one :notice
... ... @@ -19,11 +19,11 @@ class Backtrace
19 19 end
20 20  
21 21 def similar
22   - Backtrace.first(:conditions => { :fingerprint => fingerprint } )
  22 + Backtrace.find_by(:fingerprint => fingerprint) rescue nil
23 23 end
24 24  
25 25 def raw=(raw)
26   - raw.each do |raw_line|
  26 + raw.compact.each do |raw_line|
27 27 lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call)
28 28 end
29 29 end
... ...
app/models/backtrace_line.rb
1 1 class BacktraceLine
2 2 include Mongoid::Document
3   - IN_APP_PATH = %r{^\[PROJECT_ROOT\]\/(?!(vendor))}
  3 + IN_APP_PATH = %r{^\[PROJECT_ROOT\](?!(\/vendor))/?}
4 4 GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)}
5 5  
6 6 field :number, :type => Integer
  7 + field :column, :type => Integer
7 8 field :file
8 9 field :method
9 10  
... ... @@ -14,7 +15,7 @@ class BacktraceLine
14 15 delegate :app, :to => :backtrace
15 16  
16 17 def to_s
17   - "#{file}:#{number}"
  18 + "#{file_relative}:#{number}" << (column.present? ? ":#{column}" : "")
18 19 end
19 20  
20 21 def in_app?
... ...
app/models/backtrace_line_normalizer.rb
1 1 class BacktraceLineNormalizer
2 2 def initialize(raw_line)
3   - @raw_line = raw_line
  3 + @raw_line = raw_line || {}
4 4 end
5 5  
6 6 def call
... ... @@ -9,11 +9,24 @@ class BacktraceLineNormalizer
9 9  
10 10 private
11 11 def normalized_file
12   - @raw_line['file'].blank? ? "[unknown source]" : @raw_line['file'].to_s.gsub(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems')
  12 + if @raw_line['file'].blank?
  13 + "[unknown source]"
  14 + else
  15 + file = @raw_line['file'].to_s
  16 + # Detect lines from gem
  17 + file.gsub!(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems')
  18 + # Strip any query strings
  19 + file.gsub!(/\?[^\?]*$/, '')
  20 + @raw_line['file'] = file
  21 + end
13 22 end
14 23  
15 24 def normalized_method
16   - @raw_line['method'].gsub(/[0-9_]{10,}+/, "__FRAGMENT__")
  25 + if @raw_line['method'].blank?
  26 + "[unknown method]"
  27 + else
  28 + @raw_line['method'].to_s.gsub(/[0-9_]{10,}+/, "__FRAGMENT__")
  29 + end
17 30 end
18 31  
19 32 end
... ...
app/models/comment.rb
... ... @@ -6,13 +6,22 @@ class Comment
6 6 before_destroy :decrease_counter_cache
7 7  
8 8 field :body, :type => String
9   - index :user_id
  9 + index(:user_id => 1)
10 10  
11 11 belongs_to :err, :class_name => "Problem"
12 12 belongs_to :user
  13 + delegate :app, :to => :err
13 14  
14 15 validates_presence_of :body
15 16  
  17 + def notification_recipients
  18 + app.notification_recipients - [user.email]
  19 + end
  20 +
  21 + def emailable?
  22 + app.emailable? && notification_recipients.any?
  23 + end
  24 +
16 25 protected
17 26 def increase_counter_cache
18 27 err.inc(:comments_count, 1)
... ... @@ -23,4 +32,3 @@ class Comment
23 32 end
24 33  
25 34 end
26   -
... ...
app/models/comment_observer.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class CommentObserver < Mongoid::Observer
  2 + observe :comment
  3 +
  4 + def after_create(comment)
  5 + Mailer.comment_notification(comment).deliver if comment.emailable?
  6 + end
  7 +
  8 +end
... ...
app/models/deploy.rb
... ... @@ -8,7 +8,7 @@ class Deploy
8 8 field :revision
9 9 field :message
10 10  
11   - index :created_at, Mongo::DESCENDING
  11 + index(:created_at => -1)
12 12  
13 13 embedded_in :app, :inverse_of => :deploys
14 14  
... ...
app/models/err.rb
... ... @@ -6,20 +6,16 @@ class Err
6 6 include Mongoid::Document
7 7 include Mongoid::Timestamps
8 8  
9   - field :error_class, :default => "UnknownError"
10   - field :component
11   - field :action
12   - field :environment, :default => "unknown"
13 9 field :fingerprint
14 10  
15   - belongs_to :problem
16   - index :problem_id
17   - index :error_class
18   - index :fingerprint
  11 + index problem_id: 1
  12 + index fingerprint: 1
19 13  
  14 + belongs_to :problem
20 15 has_many :notices, :inverse_of => :err, :dependent => :destroy
21 16  
  17 + validates_presence_of :problem_id, :fingerprint
  18 +
22 19 delegate :app, :resolved?, :to => :problem
23 20  
24 21 end
25   -
... ...
app/models/error_report.rb
1   -require 'digest/sha1'
2 1 require 'hoptoad_notifier'
3 2  
  3 +##
  4 +# Processes a new error report.
  5 +#
  6 +# Accepts a hash with the following attributes:
  7 +#
  8 +# * <tt>:error_class</tt> - the class of error
  9 +# * <tt>:message</tt> - the error message
  10 +# * <tt>:backtrace</tt> - an array of stack trace lines
  11 +#
  12 +# * <tt>:request</tt> - a hash of values describing the request
  13 +# * <tt>:server_environment</tt> - a hash of values describing the server environment
  14 +#
  15 +# * <tt>:notifier</tt> - information to identify the source of the error report
  16 +#
4 17 class ErrorReport
5   - attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :current_user
  18 + attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :framework
6 19  
7 20 def initialize(xml_or_attributes)
8 21 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access
9 22 @attributes.each{|k, v| instance_variable_set(:"@#{k}", v) }
10 23 end
11 24  
12   - def fingerprint
13   - @fingerprint ||= Digest::SHA1.hexdigest(fingerprint_source.to_s)
14   - end
15   -
16 25 def rails_env
17   - server_environment['environment-name'] || 'development'
18   - end
19   -
20   - def component
21   - request['component'] || 'unknown'
22   - end
23   -
24   - def action
25   - request['action']
  26 + rails_env = server_environment['environment-name']
  27 + rails_env = 'development' if rails_env.blank?
  28 + rails_env
26 29 end
27 30  
28 31 def app
29   - @app ||= App.find_by_api_key!(api_key)
  32 + @app ||= App.where(:api_key => api_key).first
30 33 end
31 34  
32 35 def backtrace
... ... @@ -34,7 +37,9 @@ class ErrorReport
34 37 end
35 38  
36 39 def generate_notice!
37   - notice = Notice.new(
  40 + return unless valid?
  41 + return @notice if @notice
  42 + @notice = Notice.new(
38 43 :message => message,
39 44 :error_class => error_class,
40 45 :backtrace_id => backtrace.id,
... ... @@ -42,31 +47,35 @@ class ErrorReport
42 47 :server_environment => server_environment,
43 48 :notifier => notifier,
44 49 :user_attributes => user_attributes,
45   - :current_user => current_user
  50 + :framework => framework
46 51 )
  52 + error.notices << @notice
  53 + @notice
  54 + end
  55 + attr_reader :notice
47 56  
48   - err = app.find_or_create_err!(
  57 + ##
  58 + # Error associate to this error_report
  59 + #
  60 + # Can already exist or not
  61 + #
  62 + # @return [ Error ]
  63 + def error
  64 + @error ||= app.find_or_create_err!(
49 65 :error_class => error_class,
50   - :component => component,
51   - :action => action,
52 66 :environment => rails_env,
53   - :fingerprint => fingerprint)
  67 + :fingerprint => fingerprint
  68 + )
  69 + end
54 70  
55   - err.notices << notice
56   - notice
  71 + def valid?
  72 + !!app
57 73 end
58 74  
59 75 private
60   - def fingerprint_source
61   - {
62   - :backtrace => backtrace.id,
63   - :error_class => error_class,
64   - :component => component,
65   - :action => action,
66   - :environment => rails_env,
67   - :api_key => api_key
68   - }
  76 +
  77 + def fingerprint
  78 + @fingerprint ||= Fingerprint.generate(notice, api_key)
69 79 end
70 80  
71 81 end
72   -
... ...
app/models/fingerprint.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +require 'digest/sha1'
  2 +
  3 +class Fingerprint
  4 + attr_reader :notice, :api_key
  5 +
  6 + def self.generate(notice, api_key)
  7 + self.new(notice, api_key).to_s
  8 + end
  9 +
  10 + def initialize(notice, api_key)
  11 + @notice = notice
  12 + @api_key = api_key
  13 + end
  14 +
  15 +
  16 +
  17 + def to_s
  18 + Digest::SHA1.hexdigest(fingerprint_source.to_s)
  19 + end
  20 +
  21 + def fingerprint_source
  22 + # Find the first backtrace line with a file and line number.
  23 + if line = notice.backtrace.lines.detect {|l| l.number.present? && l.file.present? }
  24 + # If line exists, only use file and number.
  25 + file_or_message = "#{line.file}:#{line.number}"
  26 + else
  27 + # If no backtrace, use error message
  28 + file_or_message = notice.message
  29 + end
  30 +
  31 + {
  32 + :file_or_message => file_or_message,
  33 + :error_class => notice.error_class,
  34 + :component => notice.component || 'unknown',
  35 + :action => notice.action,
  36 + :environment => notice.environment_name || 'development',
  37 + :api_key => api_key
  38 + }
  39 + end
  40 +
  41 +end
... ...
app/models/issue_tracker.rb
... ... @@ -15,6 +15,15 @@ class IssueTracker
15 15 field :password, :type => String
16 16 field :ticket_properties, :type => String
17 17 field :subdomain, :type => String
  18 + field :milestone_id, :type => String
  19 +
  20 + # Is there any better way to enhance the props? Putting them into the subclass leads to
  21 + # an error while rendering the form fields -.-
  22 + field :base_url, :type => String
  23 + field :context_path, :type => String
  24 + field :issue_type, :type => String
  25 + field :issue_component, :type => String
  26 + field :issue_priority, :type => String
18 27  
19 28 validate :check_params
20 29  
... ... @@ -39,5 +48,15 @@ class IssueTracker
39 48 def configured?
40 49 project_id.present?
41 50 end
42   -end
43 51  
  52 + ##
  53 + # Update default_url_option with valid data from the request information
  54 + #
  55 + # @param [ Request ] a request with host, port and protocol
  56 + #
  57 + def self.update_url_options(request)
  58 + IssueTracker.default_url_options[:host] = request.host
  59 + IssueTracker.default_url_options[:port] = request.port
  60 + IssueTracker.default_url_options[:protocol] = request.scheme
  61 + end
  62 +end
... ...
app/models/issue_trackers/bitbucket_issues_tracker.rb
1   -class IssueTrackers::BitbucketIssuesTracker < IssueTracker
2   - Label = "bitbucket"
3   - Note = 'Please configure your Bitbucket repository in the <strong>BITBUCKET REPO</strong> field above.'
4   - Fields = [
5   - [:api_token, {
6   - :placeholder => "Your username on Bitbucket account",
7   - :label => "Username"
8   - }],
9   - [:project_id, {
10   - :placeholder => "Password for your Bitbucket account",
11   - :label => "Password"
12   - }]
13   - ]
  1 +begin
  2 + require 'bitbucket_rest_api'
  3 +rescue LoadError
  4 +end
  5 +
  6 +if defined? BitBucket
  7 + class IssueTrackers::BitbucketIssuesTracker < IssueTracker
  8 + Label = "bitbucket"
  9 + Note = 'Please configure your Bitbucket repository in the <strong>BITBUCKET REPO</strong> field above.'
  10 + Fields = [
  11 + [:api_token, {
  12 + :placeholder => "Your username on Bitbucket account",
  13 + :label => "Username"
  14 + }],
  15 + [:project_id, {
  16 + :placeholder => "Password for your Bitbucket account",
  17 + :label => "Password"
  18 + }]
  19 + ]
14 20  
15   - def check_params
16   - if Fields.detect {|f| self[f[0]].blank? }
17   - errors.add :base, 'You must specify your Bitbucket username and password'
  21 + def check_params
  22 + if Fields.detect {|f| self[f[0]].blank? }
  23 + errors.add :base, 'You must specify your Bitbucket username and password'
  24 + end
18 25 end
19   - end
20 26  
21   - def repo_name
22   - app.bitbucket_repo
23   - end
  27 + def repo_name
  28 + app.bitbucket_repo
  29 + end
24 30  
25   - def create_issue(problem, reported_by = nil)
26   - bitbucket = BitBucket.new :basic_auth => "#{api_token}:#{project_id}"
  31 + def create_issue(problem, reported_by = nil)
  32 + bitbucket = BitBucket.new :basic_auth => "#{api_token}:#{project_id}"
27 33  
28   - begin
29   - issue = bitbucket.issues.create api_token, repo_name.split('/')[1], :title => issue_title(problem), :content => body_template.result(binding), :priority => 'critical'
30   - problem.update_attributes(
31   - :issue_link => "https://bitbucket.org/#{repo_name}/issue/#{issue.local_id}/",
32   - :issue_type => Label
33   - )
34   - rescue BitBucket::Error::Unauthorized
35   - raise IssueTrackers::AuthenticationError, "Could not authenticate with BitBucket. Please check your username and password."
  34 + begin
  35 + r_user = repo_name.split('/')[0]
  36 + r_name = repo_name.split('/')[1]
  37 + issue = bitbucket.issues.create r_user, r_name, :title => issue_title(problem), :content => body_template.result(binding), :priority => 'critical'
  38 + problem.update_attributes(
  39 + :issue_link => "https://bitbucket.org/#{repo_name}/issue/#{issue.local_id}/",
  40 + :issue_type => Label
  41 + )
  42 + rescue BitBucket::Error::Unauthorized
  43 + raise IssueTrackers::AuthenticationError, "Could not authenticate with BitBucket. Please check your username and password."
  44 + end
36 45 end
37   - end
38 46  
39   - def body_template
40   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/bitbucket_issues_body.txt.erb"))
41   - end
  47 + def body_template
  48 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/bitbucket_issues_body.txt.erb"))
  49 + end
42 50  
43   - def url
44   - "https://www.bitbucket.org/#{repo_name}/issues"
  51 + def url
  52 + "https://www.bitbucket.org/#{repo_name}/issues"
  53 + end
45 54 end
46 55 end
47   -
... ...
app/models/issue_trackers/gitlab_tracker.rb
... ... @@ -10,7 +10,7 @@ if defined? Gitlab
10 10 :placeholder => "API Token for your account"
11 11 }],
12 12 [:project_id, {
13   - :label => "Ticket Project Short Name / ID",
  13 + :label => "Ticket Project ID (use Number)",
14 14 :placeholder => "Gitlab Project where issues will be created"
15 15 }]
16 16 ]
... ... @@ -23,19 +23,25 @@ if defined? Gitlab
23 23  
24 24 def create_issue(problem, reported_by = nil)
25 25 Gitlab.configure do |config|
26   - config.endpoint = "#{account}/api/v2"
  26 + config.endpoint = "#{account}/api/v3"
27 27 config.private_token = api_token
28 28 config.user_agent = 'Errbit User Agent'
29 29 end
30 30 title = issue_title problem
31   - description = body_template.result(binding)
32   - Gitlab.create_issue(project_id, title, { :description => description, :labels => "errbit" } )
  31 + description_summary = summary_template.result(binding)
  32 + description_body = body_template.result(binding)
  33 + ticket = Gitlab.create_issue(project_id, title, { :description => description_summary, :labels => "errbit" } )
  34 + Gitlab.create_issue_note(project_id, ticket.id, description_body)
  35 + end
  36 +
  37 + def summary_template
  38 + @@summary_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/gitlab_summary.txt.erb").gsub(/^\s*/, ''))
33 39 end
34 40  
35 41 def body_template
36 42 @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/gitlab_body.txt.erb").gsub(/^\s*/, ''))
37 43 end
38   -
  44 +
39 45 def url
40 46 "#{account}/#{project_id}/issues"
41 47 end
... ...
app/models/issue_trackers/jira_tracker.rb 0 → 100644
... ... @@ -0,0 +1,110 @@
  1 +if defined? JIRA
  2 + class IssueTrackers::JiraTracker < IssueTracker
  3 + Label = 'jira'
  4 +
  5 + Fields = [
  6 + [:base_url, {
  7 + :label => 'Jira URL without trailing slash',
  8 + :placeholder => 'https://jira.example.org/'
  9 + }],
  10 + [:context_path, {
  11 + :optional => true,
  12 + :label => 'Context Path (Just "/" if empty otherwise with leading slash)',
  13 + :placeholder => "/jira"
  14 + }],
  15 + [:username, {
  16 + :optional => true,
  17 + :label => 'HTTP Basic Auth User',
  18 + :placeholder => 'johndoe'
  19 + }],
  20 + [:password, {
  21 + :optional => true,
  22 + :label => 'HTTP Basic Auth Password',
  23 + :placeholder => 'p@assW0rd'
  24 + }],
  25 + [:project_id, {
  26 + :label => 'Project Key',
  27 + :placeholder => 'The project Key where the issue will be created'
  28 + }],
  29 + [:account, {
  30 + :optional => true,
  31 + :label => 'Assign to this user. If empty, Jira takes the project default.',
  32 + :placeholder => "username"
  33 + }],
  34 + [:issue_component, {
  35 + :label => 'Issue category',
  36 + :placeholder => 'Website - Other'
  37 + }],
  38 + [:issue_type, {
  39 + :label => 'Issue type',
  40 + :placeholder => 'Bug'
  41 + }],
  42 + [:issue_priority, {
  43 + :label => 'Priority',
  44 + :placeholder => 'Normal'
  45 + }]
  46 + ]
  47 +
  48 + def check_params
  49 + if Fields.detect { |f| self[f[0]].blank? && !f[1][:optional] }
  50 + errors.add :base, 'You must specify all non optional values!'
  51 + end
  52 + end
  53 +
  54 +
  55 + #
  56 + # @param problem Problem
  57 + def create_issue(problem, reported_by = nil)
  58 + options = {
  59 + :username => username,
  60 + :password => password,
  61 + :site => base_url,
  62 + :context_path => context_path,
  63 + :auth_type => :basic,
  64 + :use_ssl => base_url.match(/^https/) ? true : false
  65 + }
  66 + client = JIRA::Client.new(options)
  67 +
  68 + issue = {
  69 + :fields => {
  70 + :project => {
  71 + :key => project_id
  72 + },
  73 + :summary => issue_title(problem),
  74 + :description => body_template.result(binding),
  75 + :issuetype => {
  76 + :name => issue_type
  77 + },
  78 + :priority => {
  79 + :name => issue_priority,
  80 + },
  81 +
  82 + :components => [{:name => issue_component}]
  83 + }
  84 + }
  85 +
  86 + issue[:fields][:assignee] = {:name => account} if account
  87 +
  88 + issue_build = client.Issue.build
  89 + issue_build.save(issue)
  90 + issue_build.fetch
  91 +
  92 + problem.update_attributes(
  93 + :issue_link => "#{base_url}#{context_path}browse/#{issue_build.key}",
  94 + :issue_type => Label
  95 + )
  96 +
  97 + # Maybe in a later version?
  98 + #remote_link = {
  99 + # :url => app_problem_url(problem.app, problem),
  100 + # :name => "Link to Errbit Issue"
  101 + #}
  102 + #remote_link_build = issue_build.remotelink.build
  103 + #remote_link_build.save(remote_link)
  104 + end
  105 +
  106 + def body_template
  107 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/jira_body.txt.erb"))
  108 + end
  109 + end
  110 +end
0 111 \ No newline at end of file
... ...
app/models/issue_trackers/redmine_tracker.rb
... ... @@ -9,6 +9,12 @@ if defined? RedmineClient
9 9 [:api_token, {
10 10 :placeholder => "API Token for your account"
11 11 }],
  12 + [:username, {
  13 + :placeholder => "Your username"
  14 + }],
  15 + [:password, {
  16 + :placeholder => "Your password"
  17 + }],
12 18 [:project_id, {
13 19 :label => "Ticket Project",
14 20 :placeholder => "Redmine Project where tickets will be created"
... ... @@ -22,15 +28,19 @@ if defined? RedmineClient
22 28  
23 29 def check_params
24 30 if Fields.detect {|f| self[f[0]].blank? && !f[1][:optional]}
25   - errors.add :base, 'You must specify your Redmine URL, API token and Project ID'
  31 + errors.add :base, 'You must specify your Redmine URL, API token, Username, Password and Project ID'
26 32 end
27 33 end
28 34  
29 35 def create_issue(problem, reported_by = nil)
30 36 token = api_token
31 37 acc = account
  38 + user = username
  39 + passwd = password
32 40 RedmineClient::Base.configure do
33 41 self.token = token
  42 + self.user = user
  43 + self.password = passwd
34 44 self.site = acc
35 45 self.format = :xml
36 46 end
... ... @@ -47,17 +57,18 @@ if defined? RedmineClient
47 57 def url_to_file(file_path, line_number = nil)
48 58 # alt_project_id let's users specify a different project for tickets / app files.
49 59 project = self.alt_project_id.present? ? self.alt_project_id : self.project_id
50   - url = "#{self.account}/projects/#{project}/repository/annotate/#{file_path.sub(/^\//,'')}"
  60 + url = "#{self.account.gsub(/\/$/, '')}/projects/#{project}/repository/revisions/#{app.repository_branch}/changes/#{file_path.sub(/\[PROJECT_ROOT\]/, '').sub(/^\//,'')}"
51 61 line_number ? url << "#L#{line_number}" : url
52 62 end
53 63  
54 64 def body_template
55   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/textile_body.txt.erb"))
  65 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/redmine_body.txt.erb"))
56 66 end
57 67  
58 68 def url
59 69 acc_url = account.start_with?('http') ? account : "http://#{account}"
60   - URI.parse("#{acc_url}?project_id=#{project_id}").to_s
  70 + acc_url = acc_url.gsub(/\/$/, '')
  71 + URI.parse("#{acc_url}/projects/#{project_id}").to_s
61 72 rescue URI::InvalidURIError
62 73 end
63 74 end
... ...
app/models/issue_trackers/unfuddle_tracker.rb 0 → 100644
... ... @@ -0,0 +1,69 @@
  1 +class IssueTrackers::UnfuddleTracker < IssueTracker
  2 + Label = "unfuddle"
  3 + Fields = [
  4 +
  5 + [:account, {
  6 + :placeholder => "Your domain"
  7 + }],
  8 +
  9 +
  10 + [:username, {
  11 + :placeholder => "Your username"
  12 + }],
  13 +
  14 + [:password, {
  15 + :placeholder => "Your password"
  16 + }],
  17 +
  18 + [:project_id, {
  19 + :label => "Ticket Project",
  20 + :placeholder => "Project where tickets will be created"
  21 + }],
  22 +
  23 + [:milestone_id, {
  24 + :optional => true,
  25 + :label => "Ticket Milestone",
  26 + :placeholder => "Milestone where tickets will be created"
  27 + }]
  28 +
  29 +
  30 + ]
  31 +
  32 + def check_params
  33 + if Fields.detect {|f| self[f[0]].blank? && !f[1][:optional]}
  34 + errors.add :base, 'You must specify your Account, Username, Password and Project ID'
  35 + end
  36 + end
  37 +
  38 + def create_issue(problem, reported_by = nil)
  39 + unfuddle = TaskMapper.new(:unfuddle, :username => username, :password => password, :account => account)
  40 +
  41 + begin
  42 + issue_options = {:project_id => project_id,
  43 + :summary => issue_title(problem),
  44 + :priority => '5',
  45 + :status => "new",
  46 + :description => body_template.result(binding),
  47 + 'description-format' => 'textile' }
  48 +
  49 + issue_options[:milestone_id] = milestone_id if milestone_id.present?
  50 +
  51 + issue = unfuddle.project(project_id.to_i).ticket!(issue_options)
  52 + problem.update_attributes(
  53 + :issue_link => File.join("#{url}/tickets/#{issue['id']}"),
  54 + :issue_type => Label
  55 + )
  56 + rescue ActiveResource::UnauthorizedAccess
  57 + raise ActiveResource::UnauthorizedAccess, "Could not authenticate with Unfuddle. Please check your username and password."
  58 + end
  59 +
  60 + end
  61 +
  62 + def body_template
  63 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/textile_body.txt.erb"))
  64 + end
  65 +
  66 + def url
  67 + "https://#{account}.unfuddle.com/projects/#{project_id}"
  68 + end
  69 +end
... ...
app/models/notice.rb
1   -require 'hoptoad'
2 1 require 'recurse'
3 2  
4 3 class Notice
... ... @@ -10,23 +9,20 @@ class Notice
10 9 field :request, :type => Hash
11 10 field :notifier, :type => Hash
12 11 field :user_attributes, :type => Hash
13   - field :current_user, :type => Hash
  12 + field :framework
14 13 field :error_class
15 14 delegate :lines, :to => :backtrace, :prefix => true
16 15 delegate :app, :problem, :to => :err
17 16  
18 17 belongs_to :err
19 18 belongs_to :backtrace, :index => true
20   - index :created_at
21   - index(
22   - [
23   - [ :err_id, Mongo::ASCENDING ],
24   - [ :created_at, Mongo::ASCENDING ],
25   - [ :_id, Mongo::ASCENDING ]
26   - ]
27   - )
28   -
29   - after_create :increase_counter_cache, :cache_attributes_on_problem, :unresolve_problem
  19 +
  20 + index(:created_at => 1)
  21 + index(:err_id => 1, :created_at => 1, :_id => 1)
  22 +
  23 + after_create :cache_attributes_on_problem, :unresolve_problem
  24 + after_create :email_notification
  25 + after_create :services_notification
30 26 before_save :sanitize
31 27 before_destroy :decrease_counter_cache, :remove_cached_attributes_from_problem
32 28  
... ... @@ -42,7 +38,11 @@ class Notice
42 38 end
43 39  
44 40 def user_agent_string
45   - (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}"
  41 + if user_agent.nil? || user_agent.none?
  42 + "N/A"
  43 + else
  44 + "#{user_agent.browser} #{user_agent.version} (#{user_agent.os})"
  45 + end
46 46 end
47 47  
48 48 def environment_name
... ... @@ -98,20 +98,29 @@ class Notice
98 98 problem.notices_count
99 99 end
100 100  
101   - def notifiable?
  101 + def emailable?
102 102 app.email_at_notices.include?(similar_count)
103 103 end
104 104  
105   - def should_notify?
106   - app.notifiable? && notifiable?
  105 + def should_email?
  106 + app.emailable? && emailable?
107 107 end
108 108  
109   - protected
  109 + def should_notify?
  110 + app.notification_service.notify_at_notices.include?(0) || app.notification_service.notify_at_notices.include?(similar_count)
  111 + end
110 112  
111   - def increase_counter_cache
112   - problem.inc(:notices_count, 1)
  113 + ##
  114 + # TODO: Move on decorator maybe
  115 + #
  116 + def project_root
  117 + if server_environment
  118 + server_environment['project-root'] || ''
  119 + end
113 120 end
114 121  
  122 + protected
  123 +
115 124 def decrease_counter_cache
116 125 problem.inc(:notices_count, -1) if err
117 126 end
... ... @@ -125,7 +134,7 @@ class Notice
125 134 end
126 135  
127 136 def cache_attributes_on_problem
128   - problem.cache_notice_attributes(self)
  137 + ProblemUpdaterCache.new(problem, self).update
129 138 end
130 139  
131 140 def sanitize
... ... @@ -134,6 +143,7 @@ class Notice
134 143 end
135 144 end
136 145  
  146 +
137 147 def sanitize_hash(h)
138 148 h.recurse do
139 149 |h| h.inject({}) do |h,(k,v)|
... ... @@ -147,5 +157,25 @@ class Notice
147 157 end
148 158 end
149 159  
  160 + private
  161 +
  162 + ##
  163 + # Send email notification if needed
  164 + def email_notification
  165 + return true unless should_email?
  166 + Mailer.err_notification(self).deliver
  167 + rescue => e
  168 + HoptoadNotifier.notify(e)
  169 + end
  170 +
  171 + ##
  172 + # Launch all notification define on the app associate to this notice
  173 + def services_notification
  174 + return true unless app.notification_service_configured? and should_notify?
  175 + app.notification_service.create_notification(problem)
  176 + rescue => e
  177 + HoptoadNotifier.notify(e)
  178 + end
  179 +
150 180 end
151 181  
... ...
app/models/notice_observer.rb
... ... @@ -1,13 +0,0 @@
1   -class NoticeObserver < Mongoid::Observer
2   - observe :notice
3   -
4   - def after_create notice
5   - # if the app has a notification service, fire it off
6   - if notice.app.notification_service_configured?
7   - notice.app.notification_service.create_notification(notice.problem)
8   - end
9   -
10   - Mailer.err_notification(notice).deliver if notice.should_notify?
11   - end
12   -
13   -end
app/models/notification_service.rb
... ... @@ -5,14 +5,31 @@ class NotificationService
5 5 default_url_options[:host] = ActionMailer::Base.default_url_options[:host]
6 6  
7 7 field :room_id, :type => String
  8 + field :user_id, :type => String
  9 + field :service_url, :type => String
  10 + field :service, :type => String
8 11 field :api_token, :type => String
9 12 field :subdomain, :type => String
10 13 field :sender_name, :type => String
11   -
  14 + field :notify_at_notices, :type => Array, :default => Errbit::Config.notify_at_notices
12 15 embedded_in :app, :inverse_of => :notification_service
13 16  
14 17 validate :check_params
15 18  
  19 + if Errbit::Config.per_app_notify_at_notices
  20 + Fields = [[:notify_at_notices,
  21 + { :placeholder => 'comma separated numbers or simply 0 for every notice',
  22 + :label => 'notify on errors (0 for all errors)'
  23 + }
  24 + ]]
  25 + else
  26 + Fields = []
  27 + end
  28 +
  29 + def notify_at_notices
  30 + Errbit::Config.per_app_notify_at_notices ? super : Errbit::Config.notify_at_notices
  31 + end
  32 +
16 33 # Subclasses are responsible for overwriting this method.
17 34 def check_params; true; end
18 35  
... ... @@ -34,4 +51,8 @@ class NotificationService
34 51 def configured?
35 52 api_token.present?
36 53 end
  54 +
  55 + def problem_url(problem)
  56 + "http://#{Errbit::Config.host}/apps/#{problem.app.id}/problems/#{problem.id}"
  57 + end
37 58 end
... ...
app/models/notification_services/campfire_service.rb
1 1 if defined? Campy
2 2 class NotificationServices::CampfireService < NotificationService
3 3 Label = "campfire"
4   - Fields = [
  4 + Fields += [
5 5 [:subdomain, {
6 6 :label => "Subdomain",
7 7 :placeholder => "subdomain from http://{{subdomain}}.campfirenow.com"
... ... @@ -33,4 +33,4 @@ if defined? Campy
33 33 campy.speak "[errbit] #{problem.app.name} #{notification_description problem} - http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}/problems/#{problem.id.to_s}"
34 34 end
35 35 end
36   -end
37 36 \ No newline at end of file
  37 +end
... ...
app/models/notification_services/flowdock_service.rb 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +if defined? Flowdock
  2 + class NotificationServices::FlowdockService < NotificationService
  3 + Label = 'flowdock'
  4 + Fields += [
  5 + [
  6 + :api_token, {
  7 + :label => 'Flow API Token',
  8 + :placeholder => '123456789abcdef123456789abcdefgh'
  9 + }
  10 + ]
  11 + ]
  12 +
  13 + def check_params
  14 + if Fields.any? { |f, _| self[f].blank? }
  15 + errors.add :base, 'You must specify your Flowdock(Flow) API token'
  16 + end
  17 + end
  18 +
  19 + def url
  20 + 'https://www.flowdock.com/session'
  21 + end
  22 +
  23 + def create_notification(problem)
  24 + flow = Flowdock::Flow.new(:api_token => api_token, :source => "Errbit", :from => {:name => "Errbit", :address => ENV['ERRBIT_EMAIL_FROM'] || 'support@flowdock.com'})
  25 + subject = "[#{problem.environment}] #{problem.message.to_s.truncate(100)}"
  26 + url = app_problem_url problem.app, problem
  27 + flow.push_to_team_inbox(:subject => subject, :content => content(problem, url), :project => project_name(problem), :link => url)
  28 + end
  29 +
  30 + private
  31 +
  32 + # can only contain alphanumeric characters and underscores
  33 + def project_name(problem)
  34 + problem.app.name.gsub /[^0-9a-z_]/i, ''
  35 + end
  36 +
  37 + def content(problem, url)
  38 + full_description = "[#{ problem.environment }][#{ problem.where }] #{problem.message.to_s}"
  39 + <<-MSG.strip_heredoc
  40 + #{ERB::Util.html_escape full_description}<br>
  41 + <a href="#{url}">#{url}</a>
  42 + MSG
  43 + end
  44 + end
  45 +end
... ...
app/models/notification_services/gtalk_service.rb
1 1 class NotificationServices::GtalkService < NotificationService
2 2 Label = "gtalk"
3   - Fields = [
  3 + Fields += [
4 4 [:subdomain, {
5 5 :placeholder => "username@example.com",
6 6 :label => "Username"
... ... @@ -9,29 +9,64 @@ class NotificationServices::GtalkService &lt; NotificationService
9 9 :placeholder => "password",
10 10 :label => "Password"
11 11 }],
  12 + [:user_id, {
  13 + :placeholder => "touser@example.com, anotheruser@example.com",
  14 + :label => "Send To User(s)"
  15 + }, :room_id],
12 16 [:room_id, {
13   - :placeholder => "touser@example.com",
14   - :label => "Send To User"
  17 + :placeholder => "toroom@conference.example.com",
  18 + :label => "Send To Room (one only)"
  19 + }, :user_id],
  20 + [ :service, {
  21 + :placeholder => "talk.google.com",
  22 + :label => "Jabber Service"
15 23 }],
  24 + [ :service_url, {
  25 + :placeholder => "http://www.google.com/talk/",
  26 + :label => "Link To Jabber Service"
  27 + }]
16 28 ]
17 29  
18 30 def check_params
19   - if Fields.detect {|f| self[f[0]].blank? }
20   - errors.add :base, 'You must specify your Username, Password and To User'
  31 + if Fields.detect { |f| self[f[0]].blank? && self[f[2]].blank? }
  32 + errors.add :base,
  33 + """You must specify your Username, Password, service, service_url
  34 + and either rooms or users to send to or both"""
21 35 end
22 36 end
23 37  
24 38 def url
25   - "http://www.google.com/talk/"
  39 + service_url || "http://www.google.com/talk/"
26 40 end
27 41  
28 42 def create_notification(problem)
29 43 # build the xmpp client
30 44 client = Jabber::Client.new(Jabber::JID.new(subdomain))
31   - client.connect("talk.google.com")
  45 + client.connect(service)
32 46 client.auth(api_token)
33 47  
34   - # post the issue to the xmpp room
35   - client.send(Jabber::Message.new(room_id, "[errbit] http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} #{notification_description problem}"))
  48 + #has to look like this to be formatted properly in the client
  49 + message = """#{problem.app.name.to_s}
  50 +http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}
  51 +#{notification_description problem}"""
  52 +
  53 + # post the issue to the xmpp room(s)
  54 + send_to_users(client, message) unless user_id.blank?
  55 + send_to_muc(client, message) unless room_id.blank?
  56 + end
  57 +
  58 + private
  59 +
  60 + def send_to_users client, message
  61 + user_id.gsub(/ /i, ",").gsub(/;/i, ",").split(",").map(&:strip).reject(&:empty?).each do |user|
  62 + client.send(Jabber::Message.new(user, message))
  63 + end
  64 + end
  65 +
  66 + def send_to_muc client, message
  67 + #TODO: set this so that it can send to multiple rooms like users, nb multiple room joins in one send fail randomly so leave as one room for the moment
  68 + muc = Jabber::MUC::SimpleMUCClient.new(client)
  69 + muc.join(room_id + "/errbit")
  70 + muc.send(Jabber::Message.new(room_id, message))
36 71 end
37   -end
38 72 \ No newline at end of file
  73 +end
... ...
app/models/notification_services/hipchat_service.rb
1 1 if defined? HipChat
2 2 class NotificationServices::HipchatService < NotificationService
3 3 Label = 'hipchat'
4   - Fields = [
  4 + Fields += [
5 5 [:api_token, {
6 6 :placeholder => "API Token"
7 7 }],
... ... @@ -24,8 +24,9 @@ if defined? HipChat
24 24 def create_notification(problem)
25 25 url = app_problem_url problem.app, problem
26 26 message = <<-MSG.strip_heredoc
27   - [#{ERB::Util.html_escape problem.app.name}]#{ERB::Util.html_escape notification_description(problem)}<br>
28   - <a href="#{url}">#{url}</a>
  27 + <strong>#{ERB::Util.html_escape problem.app.name}</strong> error in <strong>#{ERB::Util.html_escape problem.environment}</strong> at <strong>#{ERB::Util.html_escape problem.where}</strong> (<a href="#{url}">details</a>)<br>
  28 + &nbsp;&nbsp;#{ERB::Util.html_escape problem.message.to_s.truncate(100)}<br>
  29 + &nbsp;&nbsp;Times occurred: #{problem.notices_count}
29 30 MSG
30 31  
31 32 client = HipChat::Client.new(api_token)
... ...
app/models/notification_services/hoiio_service.rb
1 1 class NotificationServices::HoiioService < NotificationService
2 2 Label = "hoiio"
3   - Fields = [
  3 + Fields += [
4 4 [:api_token, {
5 5 :placeholder => "App ID",
6 6 :label => "App ID"
... ... @@ -39,4 +39,4 @@ class NotificationServices::HoiioService &lt; NotificationService
39 39 end
40 40  
41 41 end
42   -end
43 42 \ No newline at end of file
  43 +end
... ...
app/models/notification_services/hubot_service.rb 0 → 100644
... ... @@ -0,0 +1,32 @@
  1 +class NotificationServices::HubotService < NotificationService
  2 + Label = "hubot"
  3 + Fields += [
  4 + [:api_token, {
  5 + :placeholder => 'http://hubot.example.org:8080/hubot/say',
  6 + :label => 'Hubot URL'
  7 + }],
  8 + [:room_id, {
  9 + :placeholder => '#dev',
  10 + :label => 'Room where Hubot should notify'
  11 + }]
  12 + ]
  13 +
  14 + def check_params
  15 + if Fields.detect {|f| self[f[0]].blank? }
  16 + errors.add :base, 'You must specify the URL of your hubot'
  17 + end
  18 + end
  19 +
  20 + def url
  21 + api_token
  22 + end
  23 +
  24 + def message_for_hubot(problem)
  25 + "[#{problem.app.name}][#{problem.environment}][#{problem.where}]: #{problem.error_class} #{problem_url(problem)}"
  26 + end
  27 +
  28 + def create_notification(problem)
  29 + HTTParty.post(url, :body => {:message => message_for_hubot(problem), :room => room_id})
  30 + end
  31 +end
  32 +
... ...
app/models/notification_services/pushover_service.rb
1 1 class NotificationServices::PushoverService < NotificationService
2 2 Label = "pushover"
3   - Fields = [
  3 + Fields += [
4 4 [:api_token, {
5 5 :placeholder => "User Key",
6 6 :label => "User Key"
... ... @@ -29,4 +29,4 @@ class NotificationServices::PushoverService &lt; NotificationService
29 29 notification.notify(api_token, "#{notification_description problem}", :priority => 1, :title => "Errbit Notification", :url => "http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}", :url_title => "Link to error")
30 30  
31 31 end
32   -end
33 32 \ No newline at end of file
  33 +end
... ...
app/models/notification_services/webhook_service.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +class NotificationServices::WebhookService < NotificationService
  2 + Label = "webhook"
  3 + Fields = [
  4 + [:api_token, {
  5 + :placeholder => 'URL to receive a POST request when an error occurs',
  6 + :label => 'URL'
  7 + }]
  8 + ]
  9 +
  10 + def check_params
  11 + if Fields.detect {|f| self[f[0]].blank? }
  12 + errors.add :base, 'You must specify the URL'
  13 + end
  14 + end
  15 +
  16 + def create_notification(problem)
  17 + HTTParty.post(api_token, :body => {:problem => problem.to_json})
  18 + end
  19 +end
... ...
app/models/problem.rb
... ... @@ -26,29 +26,39 @@ class Problem
26 26 field :hosts, :type => Hash, :default => {}
27 27 field :comments_count, :type => Integer, :default => 0
28 28  
29   - index :app_id
30   - index :app_name
31   - index :message
32   - index :last_notice_at
33   - index :first_notice_at
34   - index :last_deploy_at
35   - index :resolved_at
36   - index :notices_count
  29 + index :app_id => 1
  30 + index :app_name => 1
  31 + index :message => 1
  32 + index :last_notice_at => 1
  33 + index :first_notice_at => 1
  34 + index :last_deploy_at => 1
  35 + index :resolved_at => 1
  36 + index :notices_count => 1
37 37  
38 38 belongs_to :app
39 39 has_many :errs, :inverse_of => :problem, :dependent => :destroy
40 40 has_many :comments, :inverse_of => :err, :dependent => :destroy
41 41  
  42 + validates_presence_of :environment
  43 +
42 44 before_create :cache_app_attributes
43 45  
44 46 scope :resolved, where(:resolved => true)
45 47 scope :unresolved, where(:resolved => false)
46 48 scope :ordered, order_by(:last_notice_at.desc)
47 49 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}
48   -
  50 +
49 51 validates_presence_of :last_notice_at, :first_notice_at
50 52  
51 53  
  54 + def self.all_else_unresolved(fetch_all)
  55 + if fetch_all
  56 + all
  57 + else
  58 + where(:resolved => false)
  59 + end
  60 + end
  61 +
52 62 def self.in_env(env)
53 63 env.present? ? where(:environment => env) : scoped
54 64 end
... ... @@ -75,15 +85,7 @@ class Problem
75 85  
76 86  
77 87 def self.merge!(*problems)
78   - problems = problems.flatten.uniq
79   - merged_problem = problems.shift
80   - problems.each do |problem|
81   - merged_problem.errs.concat Err.where(:problem_id => problem.id)
82   - problem.errs(true) # reload problem.errs (should be empty) before problem.destroy
83   - problem.destroy
84   - end
85   - merged_problem.reset_cached_attributes
86   - merged_problem
  88 + ProblemMerge.new(problems).merge
87 89 end
88 90  
89 91 def merged?
... ... @@ -91,11 +93,12 @@ class Problem
91 93 end
92 94  
93 95 def unmerge!
  96 + attrs = {:error_class => error_class, :environment => environment}
94 97 problem_errs = errs.to_a
95 98 problem_errs.shift
96 99 [self] + problem_errs.map(&:id).map do |err_id|
97 100 err = Err.find(err_id)
98   - app.problems.create.tap do |new_problem|
  101 + app.problems.create(attrs).tap do |new_problem|
99 102 err.update_attribute(:problem_id, new_problem.id)
100 103 new_problem.reset_cached_attributes
101 104 end
... ... @@ -113,16 +116,14 @@ class Problem
113 116 else raise("\"#{sort}\" is not a recognized sort")
114 117 end
115 118 end
116   -
  119 +
117 120 def self.in_date_range(date_range)
118 121 where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}])
119 122 end
120 123  
121 124  
122 125 def reset_cached_attributes
123   - update_attribute(:notices_count, notices.count)
124   - cache_app_attributes
125   - cache_notice_attributes
  126 + ProblemUpdaterCache.new(self).update
126 127 end
127 128  
128 129 def cache_app_attributes
... ... @@ -131,32 +132,12 @@ class Problem
131 132 self.last_deploy_at = if (last_deploy = app.deploys.where(:environment => self.environment).last)
132 133 last_deploy.created_at.utc
133 134 end
134   - collection.update({'_id' => self.id},
135   - {'$set' => {'app_name' => self.app_name,
  135 + collection.find('_id' => self.id)
  136 + .update({'$set' => {'app_name' => self.app_name,
136 137 'last_deploy_at' => self.last_deploy_at.try(:utc)}})
137 138 end
138 139 end
139 140  
140   - def cache_notice_attributes(notice=nil)
141   - first_notice = notices.order_by([:created_at, :asc]).first
142   - last_notice = notices.order_by([:created_at, :asc]).last
143   - notice ||= first_notice
144   -
145   - attrs = {}
146   - attrs[:first_notice_at] = first_notice.created_at if first_notice
147   - attrs[:last_notice_at] = last_notice.created_at if last_notice
148   - attrs.merge!(
149   - :message => notice.message,
150   - :environment => notice.environment_name,
151   - :error_class => notice.error_class,
152   - :where => notice.where,
153   - :messages => attribute_count_increase(:messages, notice.message),
154   - :hosts => attribute_count_increase(:hosts, notice.host),
155   - :user_agents => attribute_count_increase(:user_agents, notice.user_agent_string)
156   - ) if notice
157   - update_attributes!(attrs)
158   - end
159   -
160 141 def remove_cached_notice_attributes(notice)
161 142 update_attributes!(
162 143 :messages => attribute_count_descrease(:messages, notice.message),
... ... @@ -171,16 +152,17 @@ class Problem
171 152 (app.issue_tracker_configured? && app.issue_tracker.label) || nil
172 153 end
173 154  
  155 + def self.search(value)
  156 + any_of(
  157 + {:error_class => /#{value}/i},
  158 + {:where => /#{value}/i},
  159 + {:message => /#{value}/i},
  160 + {:app_name => /#{value}/i},
  161 + {:environment => /#{value}/i}
  162 + )
  163 + end
  164 +
174 165 private
175   - def attribute_count_increase(name, value)
176   - counter, index = send(name), attribute_index(value)
177   - if counter[index].nil?
178   - counter[index] = {'value' => value, 'count' => 1}
179   - else
180   - counter[index]['count'] += 1
181   - end
182   - counter
183   - end
184 166  
185 167 def attribute_count_descrease(name, value)
186 168 counter, index = send(name), attribute_index(value)
... ... @@ -195,6 +177,5 @@ class Problem
195 177 def attribute_index(value)
196 178 Digest::MD5.hexdigest(value.to_s)
197 179 end
198   -
199 180 end
200 181  
... ...
app/models/user.rb
... ... @@ -13,14 +13,33 @@ class User
13 13 field :per_page, :type => Fixnum, :default => PER_PAGE
14 14 field :time_zone, :default => "UTC"
15 15  
16   - after_destroy :destroy_watchers
  16 + ## Devise field
  17 + ### Database Authenticatable
  18 + field :encrypted_password, :type => String
  19 +
  20 + ### Recoverable
  21 + field :reset_password_token, :type => String
  22 + field :reset_password_sent_at, :type => Time
  23 +
  24 + ### Rememberable
  25 + field :remember_created_at, :type => Time
  26 +
  27 + ### Trackable
  28 + field :sign_in_count, :type => Integer
  29 + field :current_sign_in_at, :type => Time
  30 + field :last_sign_in_at, :type => Time
  31 + field :current_sign_in_ip, :type => String
  32 + field :last_sign_in_ip, :type => String
  33 +
  34 + ### Token_authenticatable
  35 + field :authentication_token, :type => String
  36 +
  37 +
17 38 before_save :ensure_authentication_token
18 39  
19 40 validates_presence_of :name
20 41 validates_uniqueness_of :github_login, :allow_nil => true
21 42  
22   - attr_protected :admin
23   -
24 43 has_many :apps, :foreign_key => 'watchers.user_id'
25 44  
26 45 if Errbit::Config.user_has_username
... ... @@ -52,10 +71,12 @@ class User
52 71 github_account? && Errbit::Config.github_access_scope.include?('repo')
53 72 end
54 73  
55   - protected
56   -
57   - def destroy_watchers
58   - watchers.each(&:destroy)
  74 + def github_login=(login)
  75 + if login.is_a?(String) && login.strip.empty?
  76 + login = nil
59 77 end
  78 + self[:github_login] = login
  79 + end
  80 +
60 81 end
61 82  
... ...
app/views/apps/_fields.html.haml
1   -= errors_for @app
  1 += errors_for app
2 2  
3 3 %div.required
4 4 = f.label :name
... ... @@ -13,6 +13,10 @@
13 13 %div
14 14 = f.label :bitbucket_repo
15 15 = f.text_field :bitbucket_repo, :placeholder => "errbit/errbit from https://bitbucket.org/errbit/errbit"
  16 +%div
  17 + = f.label :asset_host
  18 + %em Used to generate links for JavaScript errors
  19 + = f.text_field :asset_host, :placeholder => "e.g. https://assets.example.com"
16 20  
17 21 %fieldset
18 22 %legend Notifications
... ...
app/views/apps/_service_notification_fields.html.haml
... ... @@ -17,7 +17,8 @@
17 17 - notification_service::Fields.each do |field, field_info|
18 18 = w.label field, field_info[:label] || field.to_s.titleize
19 19 - field_type = field == :password ? :password_field : :text_field
20   - = w.send field_type, field, :placeholder => field_info[:placeholder], :value => w.object.send(field)
  20 + - value = field == :notify_at_notices ? w.object.notify_at_notices.join(", ") : w.object.send(field)
  21 + = w.send field_type, field, :placeholder => field_info[:placeholder], :value => value
21 22  
22 23 .image_preloader
23 24 - (NotificationService.subclasses.map{|t| t.label } << 'none').each do |notification_service|
... ...
app/views/apps/edit.html.haml
1 1 - content_for :title, 'Edit App'
2 2 - content_for :action_bar do
3 3 = link_to_copy_attributes_from_other_app
4   - = link_to('cancel', app_path(@app), :class => 'button')
  4 + = link_to 'destroy application', app_path(app), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button'
  5 + = link_to('cancel', app_path(app), :class => 'button')
5 6  
6   -= form_for @app do |f|
  7 += form_for app do |f|
7 8  
8 9 = render 'fields', :f => f
9 10  
... ...
app/views/apps/index.html.haml
1   -- content_for :title, 'Apps'
  1 +- content_for :title, t('.title')
2 2 - content_for :action_bar do
3   - %span= link_to('Add a New App', new_app_path, :class => 'add') if current_user.admin?
  3 + %span= link_to(t('.new_app'), new_app_path, :class => 'add') if current_user.admin?
4 4  
5 5 %table.apps
6 6 %thead
7 7 %tr
8   - %th Name
  8 + %th= t('.name')
9 9 - if any_github_repos? || any_bitbucket_repos?
10   - %th Repository
  10 + %th= t('.repository')
11 11 - if any_notification_services?
12   - %th Notification Service
  12 + %th= t('.notify')
13 13 - if any_issue_trackers?
14   - %th Tracker
  14 + %th= t('.tracker')
15 15 - if any_deploys?
16   - %th Last Deploy
17   - %th Errors
  16 + %th= t('.last_deploy')
  17 + %th=t('.errors')
18 18 %tbody
19   - - @apps.each do |app|
  19 + - apps.each do |app|
20 20 %tr
21 21 %td.name= link_to app.name, app_path(app)
22 22 - if any_github_repos? or any_bitbucket_repos?
... ... @@ -50,10 +50,10 @@
50 50 - if app.problem_count > 0
51 51 - unresolved = app.unresolved_count
52 52 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
53   - - if @apps.none?
  53 + - if apps.none?
54 54 %tr
55 55 %td{:colspan => 3}
56 56 %em
57   - No apps here.
58   - = link_to 'Click here to create your first one', new_app_path
  57 + = t('.no_apps')
  58 + = link_to t('.click_to_create'), new_app_path
59 59  
... ...
app/views/apps/new.html.haml
... ... @@ -3,7 +3,7 @@
3 3 = link_to_copy_attributes_from_other_app
4 4 = link_to('cancel', apps_path, :class => 'button')
5 5  
6   -= form_for @app do |f|
  6 += form_for app do |f|
7 7  
8 8 = render 'fields', :f => f
9 9  
... ...
app/views/apps/show.atom.builder
1 1 atom_feed do |feed|
2   - feed.title("Errbit notices for #{h @app.name} at #{root_url}")
  2 + feed.title("Errbit notices for #{h app.name} at #{root_url}")
3 3 render "problems/list", :feed => feed
4 4 end
... ...
app/views/apps/show.html.haml
1   -- content_for :title, @app.name
  1 +- content_for :title, app.name
2 2 - content_for :head do
3   - = auto_discovery_link_tag :atom, app_path(@app, User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices for #{@app.name} at #{request.host}"
  3 + = auto_discovery_link_tag :atom, app_path(app, User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices for #{app.name} at #{request.host}"
4 4 - content_for :meta do
5 5 %strong Errors Caught:
6   - = @app.problems.count
  6 + = app.problems.count
7 7 %strong Deploy Count:
8   - = @app.deploys.count
  8 + = app.deploys.count
9 9 %strong API Key:
10   - = @app.api_key
  10 + = app.api_key
11 11 - content_for :action_bar do
12 12 - if current_user.admin?
13   - = link_to 'edit', edit_app_path(@app), :class => 'button'
14   - = link_to 'destroy', app_path(@app), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button'
15   - - if @all_errs
16   - = link_to 'unresolved errs', app_path(@app), :class => 'button'
  13 + = link_to 'edit', edit_app_path(app), :class => 'button'
  14 + - if all_errs
  15 + = link_to 'unresolved errs', app_path(app), :class => 'button'
17 16 - else
18   - = link_to 'all errs', app_path(@app, :all_errs => true), :class => 'button'
  17 + = link_to 'all errs', app_path(app, :all_errs => true), :class => 'button'
19 18 = link_to 'unwatch', app_watcher_path({:app_id => @app, :id => current_user.id}), :method => :delete, :class => 'button', :confirm => 'Are you sure?'
  19 +
20 20 %h3#watchers_toggle
21 21 Watchers
22 22 %span.click_span (show/hide)
23 23 #watchers_div
24   - - if @app.notify_all_users
  24 + - if app.notify_all_users
25 25 %table.watchers
26 26 %thead
27 27 %tr
... ... @@ -32,15 +32,15 @@
32 32 %tr
33 33 %th User or Email
34 34 %tbody
35   - - @app.watchers.each do |watcher|
  35 + - app.watchers.each do |watcher|
36 36 %tr
37 37 %td= watcher.label
38   - - if @app.watchers.none?
  38 + - if app.watchers.none?
39 39 %tr
40 40 %td
41 41 %em Sadly, no one is watching this app
42 42  
43   -- if @app.github_repo?
  43 +- if app.github_repo?
44 44 %h3#repository_toggle
45 45 Repository
46 46 %span.click_span (show/hide)
... ... @@ -51,13 +51,13 @@
51 51 %th GitHub Repo
52 52 %tbody
53 53 %tr
54   - %td= link_to(@app.github_repo, @app.github_url, :target => '_blank')
  54 + %td= link_to(app.github_repo, app.github_url, :target => '_blank')
55 55  
56 56 %h3#deploys_toggle
57 57 Latest Deploys
58 58 %span.click_span (show/hide)
59 59 #deploys_div
60   - - if @deploys.any?
  60 + - if deploys.any?
61 61 %table.deploys
62 62 %thead
63 63 %tr
... ... @@ -69,7 +69,7 @@
69 69 %th Revision
70 70  
71 71 %tbody
72   - - @deploys.each do |deploy|
  72 + - deploys.each do |deploy|
73 73 %tr
74 74 %td.when #{deploy.created_at.to_s(:micro)}
75 75 %td.environment #{deploy.environment}
... ... @@ -77,14 +77,20 @@
77 77 %td.message #{deploy.message}
78 78 %td.repository #{deploy.repository}
79 79 %td.revision #{deploy.short_revision}
80   - = link_to "All Deploys (#{@app.deploys.count})", app_deploys_path(@app), :class => 'button'
  80 + = link_to "All Deploys (#{app.deploys.count})", app_deploys_path(app), :class => 'button'
81 81 - else
82 82 %h3 No deploys
83 83  
84   -- if @app.problems.any?
  84 +- if app.problems.any?
85 85 %h3.clear Errors
86   - = render 'problems/table', :problems => @problems
  86 + %section
  87 + = form_tag search_problems_path(:all_errs => all_errs, :app_id => app.id), :method => :get, :remote => true do
  88 + = text_field_tag :search, params[:search], :placeholder => 'Search for issues'
  89 + %br
  90 + %section
  91 + .problem_table{:id => 'problem_table'}
  92 + = render 'problems/table', :problems => problems
87 93 - else
88 94 %h3.clear No errs have been caught yet, make sure you setup your app
89   - = render 'configuration_instructions', :app => @app
  95 + = render 'configuration_instructions', :app => app
90 96  
... ...
app/views/devise/sessions/new.html.haml
... ... @@ -9,7 +9,7 @@
9 9 = form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f|
10 10 .required
11 11 = f.label auth_key
12   - = f.text_field auth_key, :tabindex => 1
  12 + = f.text_field auth_key, :type => (Errbit::Config.user_has_username ? 'text' : 'email'), :tabindex => 1
13 13  
14 14 .required
15 15 = link_to 'forget it?', new_password_path(resource_name), :class => 'float-right', :id => "forgot_password"
... ...
app/views/issue_trackers/gitlab_body.txt.erb
1   -[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
2 1 <% if notice = problem.notices.first %>
3   -# <%= notice.message %> #
4   -## Summary ##
5   -<% if notice.request['url'].present? %>
6   - ### URL ###
7   - [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8   -<% end %>
9   -### Where ###
10   -<%= notice.where %>
11   -
12   -### Occured ###
13   -<%= notice.created_at.to_s(:micro) %>
14   -
15   -### Similar ###
16   -<%= (notice.problem.notices_count - 1).to_s %>
17   -
18 2 ## Params ##
19 3 ```
20 4 <%= pretty_hash(notice.params) %>
... ...
app/views/issue_trackers/gitlab_summary.txt.erb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
  2 +<% if notice = problem.notices.first %>
  3 +# <%= notice.message %> #
  4 +## Summary ##
  5 +<% if notice.request['url'].present? %>
  6 + ### URL ###
  7 + [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
  8 +<% end %>
  9 +### Where ###
  10 +<%= notice.where %>
  11 +
  12 +### Occured ###
  13 +<%= notice.created_at.to_s(:micro) %>
  14 +
  15 +### Similar ###
  16 +<%= (notice.problem.notices_count - 1).to_s %>
  17 +<% end %>
0 18 \ No newline at end of file
... ...
app/views/issue_trackers/jira_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +<% if notice = problem.notices.first %>
  2 +h2. Summary
  3 +<% if notice.request['url'].present? %>
  4 +h3. URL
  5 +
  6 +"<%= notice.request['url'] %>":<%= notice.request['url'] %>
  7 +<% end %>
  8 +h3. Where
  9 +
  10 +<%= notice.where %>
  11 +
  12 +h3. When
  13 +
  14 +<%= notice.created_at.to_s(:micro) %>
  15 +
  16 +"More Details on Errbit":<%= app_problem_url problem.app, problem %>
  17 +<% end %>
0 18 \ No newline at end of file
... ...
app/views/issue_trackers/redmine_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +<% if notice = problem.notices.first %>
  2 +h2. Summary
  3 +<% if notice.request['url'].present? %>
  4 +h3. URL
  5 +
  6 +"<%= notice.request['url'] %>":<%= notice.request['url'] %>
  7 +<% end %>
  8 +h3. Where
  9 +
  10 +<%= notice.where %>
  11 +
  12 +h3. When
  13 +
  14 +<%= notice.created_at.to_s(:micro) %>
  15 +
  16 +"More Details on Errbit":<%= app_problem_url problem.app, problem %>
  17 +<% end %>
0 18 \ No newline at end of file
... ...
app/views/kaminari/notices/_paginator.html.haml
... ... @@ -6,9 +6,9 @@
6 6 -# remote: data-remote
7 7 -# paginator: the paginator that renders the pagination tags inside
8 8 = paginator.render do
9   - .notice-pagination<
  9 + .notice-pagination
10 10 = next_page_tag
11   - |&nbsp;
  11 + |
12 12 = prev_page_tag
13 13 .notice-pagination-loader= image_tag 'loader.gif'
14   -viewing occurrence #{page_count_from_end(current_page, total_pages)} of #{total_pages}
  14 +%div viewing occurrence #{page_count_from_end(current_page, total_pages)} of #{total_pages}
... ...
app/views/layouts/application.html.haml
... ... @@ -2,7 +2,8 @@
2 2 %html
3 3 %head
4 4 %title
5   - Errbit &mdash;
  5 + = t('.title')
  6 + &mdash;
6 7 = yield(:page_title).present? ? yield(:page_title) : yield(:title)
7 8 %meta{ :content => "text/html; charset=utf-8", "http-equiv" => "content-type" }/
8 9 = favicon_link_tag
... ... @@ -14,7 +15,7 @@
14 15 %body{:id => controller.controller_name, :class => controller.action_name}
15 16 #header
16 17 %div
17   - = link_to 'Errbit', root_path, :id => 'site-name'
  18 + = link_to t('.errbit'), root_path, :id => 'site-name'
18 19 = render 'shared/navigation' if current_user
19 20 = render 'shared/session'
20 21 #content-wrapper
... ... @@ -30,5 +31,5 @@
30 31 - if content_for?(:comments)
31 32 #content-comments
32 33 = yield :comments
33   - #footer Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.
  34 + #footer= t('.powered_html', :link => link_to(t('.errbit'), 'http://github.com/errbit/errbit', :target => '_blank'))
34 35 = yield :scripts
... ...
app/views/mailer/comment_notification.html.haml 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +%tr
  2 + %td.section
  3 + %table(cellpadding="0" cellspacing="0" border="0" align="left")
  4 + %tbody
  5 + %tr
  6 + %td.content(valign="top")
  7 + %div
  8 + %p
  9 + = @user.name
  10 + has just commented on an error that occurred in
  11 + = link_to(@app.name, app_url(@app), :class => "bold") << ","
  12 + on the
  13 + %span.bold= @problem.environment
  14 + environment.
  15 + %br
  16 + This err has occurred #{pluralize @problem.notices_count, 'time'}.
  17 + %p
  18 + = link_to("Click here to view the error and add a comment on Errbit", app_problem_url(@app, @problem), :class => "bold") << "."
  19 +
  20 +%tr
  21 + %td.section
  22 + %table(cellpadding="0" cellspacing="0" border="0" align="left")
  23 + %tbody
  24 + %tr
  25 + %td.content(valign="top")
  26 + %div
  27 + %p.heading COMMENT:
  28 + %br
  29 + %p= @comment.body.to_s.gsub("\n", "<br/>").html_safe
  30 +
  31 +%tr
  32 + %td.section
  33 + %table(cellpadding="0" cellspacing="0" border="0" align="left")
  34 + %tbody
  35 + %tr
  36 + %td.content(valign="top")
  37 + %div
  38 + %p.heading ERROR MESSAGE:
  39 + %p= @problem.message
  40 + %p.heading WHERE:
  41 + %p.monospace
  42 + = @problem.where
  43 + %p.heading URL:
  44 + %p.monospace
  45 + - if @notice.request['url'].present?
  46 + = link_to @notice.request['url'], @notice.request['url']
  47 + %p.heading BROWSER:
  48 + %p.monospace
  49 + = user_agent_graph(@problem)
  50 + %br
... ...
app/views/mailer/comment_notification.text.erb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +<%= @user.name %> has just commented on an error that occurred in <%= @notice.environment_name %>: <%= raw(@notice.message) %>
  2 +
  3 +This err has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here:
  4 +
  5 + <%= app_problem_url(@app, @notice.problem) %>
  6 +
  7 +
  8 +COMMENT:
  9 +
  10 +<%= @comment.body %>
  11 +
  12 +
  13 +-----------------------------------------------
  14 +
  15 +ERROR MESSAGE:
  16 +
  17 +<%= raw(@notice.message) %>
  18 +
  19 +
  20 +WHERE:
  21 +
  22 +<%= @notice.where %>
  23 +
  24 +<% @notice.in_app_backtrace_lines.each do |line| %>
  25 + <%= line %>
  26 +<% end %>
  27 +
  28 +
  29 +URL:
  30 +
  31 +<%= @notice.request['url'] %>
  32 +
  33 +
  34 +BROWSER:
  35 +
  36 +<%= @notice.user_agent_string %>
... ...