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.

@@ -13,3 +13,9 @@ config/newrelic.yml @@ -13,3 +13,9 @@ config/newrelic.yml
13 *~ 13 *~
14 *.rbc 14 *.rbc
15 .DS_Store 15 .DS_Store
  16 +*.rbx
  17 +bin
  18 +bundle
  19 +coverage
  20 +*#
  21 +.ruby-version
@@ -1,4 +0,0 @@ @@ -1,4 +0,0 @@
1 ---colour  
2 ---tty  
3 ---drb  
4 ---format documentation  
1 language: ruby 1 language: ruby
  2 +env:
  3 + - COVERAGE=true
2 rvm: 4 rvm:
  5 + - 2.0.0
3 - 1.9.3 6 - 1.9.3
4 - - 1.9.2  
5 - - 1.8.7 7 + - rbx-19mode
6 services: mongodb 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 # To stop Travis from running tests for a new commit, 14 # To stop Travis from running tests for a new commit,
9 # add the following to your commit message: [ci skip] 15 # add the following to your commit message: [ci skip]
CHANGELOG.md 0 → 100644
@@ -0,0 +1,155 @@ @@ -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 @@ @@ -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
1 source 'http://rubygems.org' 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 gem 'haml' 8 gem 'haml'
8 -gem 'htmlentities', "~> 4.3.0" 9 +gem 'htmlentities'
9 gem 'rack-ssl', :require => 'rack/ssl' # force SSL 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 gem 'SystemTimer', :platform => :ruby_18 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 gem 'fabrication', "~> 1.3.0" # Used for both tests and demo data 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 # Please don't update hoptoad_notifier to airbrake. 22 # Please don't update hoptoad_notifier to airbrake.
20 # It's for internal use only, and we monkeypatch certain methods 23 # It's for internal use only, and we monkeypatch certain methods
21 gem 'hoptoad_notifier', "~> 2.4" 24 gem 'hoptoad_notifier', "~> 2.4"
@@ -35,68 +38,87 @@ gem &#39;pivotal-tracker&#39; @@ -35,68 +38,87 @@ gem &#39;pivotal-tracker&#39;
35 # Fogbugz 38 # Fogbugz
36 gem 'ruby-fogbugz', :require => 'fogbugz' 39 gem 'ruby-fogbugz', :require => 'fogbugz'
37 # Github Issues 40 # Github Issues
38 -gem 'octokit', '~> 1.0.0' 41 +gem 'octokit'
39 # Gitlab 42 # Gitlab
40 -gem 'gitlab' 43 +gem 'gitlab', :git => 'https://github.com/NARKOZ/gitlab.git'
41 44
42 # Bitbucket Issues 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 # Notification services 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 # Hipchat 59 # Hipchat
50 gem 'hipchat' 60 gem 'hipchat'
51 # Google Talk 61 # Google Talk
52 -gem 'xmpp4r' 62 +gem 'xmpp4r', :require => ["xmpp4r", "xmpp4r/muc"]
53 # Hoiio (SMS) 63 # Hoiio (SMS)
54 gem 'hoi' 64 gem 'hoi'
55 # Pushover (iOS Push notifications) 65 # Pushover (iOS Push notifications)
56 gem 'rushover' 66 gem 'rushover'
  67 +# Hubot
  68 +gem 'httparty'
  69 +# Flowdock
  70 +gem 'flowdock'
57 71
58 # Authentication 72 # Authentication
59 # --------------------------------------- 73 # ---------------------------------------
60 # GitHub OAuth 74 # GitHub OAuth
61 gem 'omniauth-github' 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 gem 'ri_cal' 77 gem 'ri_cal'
71 gem 'yajl-ruby', :require => "yajl" 78 gem 'yajl-ruby', :require => "yajl"
72 79
73 group :development, :test do 80 group :development, :test do
74 gem 'rspec-rails', '~> 2.6' 81 gem 'rspec-rails', '~> 2.6'
75 gem 'webmock', :require => false 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 # gem 'rpm_contrib' 87 # gem 'rpm_contrib'
82 # gem 'newrelic_rpm' 88 # gem 'newrelic_rpm'
  89 + gem 'quiet_assets'
  90 +end
  91 +
  92 +group :development do
83 gem 'capistrano' 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 end 104 end
85 105
86 group :test do 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 gem 'launchy' 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 gem 'email_spec' 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 end 116 end
93 117
94 group :heroku, :production do 118 group :heroku, :production do
95 gem 'unicorn' 119 gem 'unicorn'
96 end 120 end
97 121
98 -# Use thin for development  
99 -gem 'thin', :group => :development, :platform => :ruby  
100 122
101 # Gems used only for assets and not required 123 # Gems used only for assets and not required
102 # in production environments by default. 124 # in production environments by default.
@@ -104,7 +126,10 @@ group :assets do @@ -104,7 +126,10 @@ group :assets do
104 gem 'execjs' 126 gem 'execjs'
105 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows 127 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows
106 gem 'uglifier', '>= 1.0.3' 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 gem 'underscore-rails' 133 gem 'underscore-rails'
  134 + gem 'turbo-sprockets-rails3'
108 end 135 end
109 -  
110 -gem 'turbo-sprockets-rails3'  
  1 +GIT
  2 + remote: https://github.com/NARKOZ/gitlab.git
  3 + revision: 7a00d38c53335010d2fb8a233247fe2c97338903
  4 + specs:
  5 + gitlab (2.2.0)
  6 + httparty
  7 +
1 GEM 8 GEM
2 remote: http://rubygems.org/ 9 remote: http://rubygems.org/
3 specs: 10 specs:
4 SystemTimer (1.2.3) 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 actionmailer (>= 3.0.0) 16 actionmailer (>= 3.0.0)
10 nokogiri (>= 1.4.4) 17 nokogiri (>= 1.4.4)
11 premailer (>= 1.7.1) 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 builder (~> 3.0.0) 22 builder (~> 3.0.0)
16 erubis (~> 2.7.0) 23 erubis (~> 2.7.0)
17 journey (~> 1.0.4) 24 journey (~> 1.0.4)
18 - rack (~> 1.4.0) 25 + rack (~> 1.4.5)
19 rack-cache (~> 1.2) 26 rack-cache (~> 1.2)
20 rack-test (~> 0.6.1) 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 builder (~> 3.0.0) 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 arel (~> 3.0.2) 35 arel (~> 3.0.2)
29 tzinfo (~> 0.3.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 multi_json (~> 1.0) 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 arel (3.0.2) 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 faraday (~> 0.8.1) 55 faraday (~> 0.8.1)
41 faraday_middleware (~> 0.8.1) 56 faraday_middleware (~> 0.8.1)
42 hashie (~> 1.2.0) 57 hashie (~> 1.2.0)
43 multi_json (~> 1.3) 58 multi_json (~> 1.3)
44 nokogiri (~> 1.5.2) 59 nokogiri (~> 1.5.2)
45 simple_oauth 60 simple_oauth
46 - bson (1.6.2)  
47 - bson_ext (1.6.2)  
48 - bson (~> 1.6.2)  
49 builder (3.0.4) 61 builder (3.0.4)
  62 + callsite (0.0.11)
50 campy (0.1.3) 63 campy (0.1.3)
51 multi_json (~> 1.0) 64 multi_json (~> 1.0)
52 - capistrano (2.13.5) 65 + capistrano (2.15.5)
53 highline 66 highline
54 net-scp (>= 1.0.0) 67 net-scp (>= 1.0.0)
55 net-sftp (>= 2.0.0) 68 net-sftp (>= 2.0.0)
56 net-ssh (>= 2.0.14) 69 net-ssh (>= 2.0.14)
57 net-ssh-gateway (>= 1.1.0) 70 net-ssh-gateway (>= 1.1.0)
58 - capybara (1.1.2) 71 + capybara (2.0.3)
59 mime-types (>= 1.16) 72 mime-types (>= 1.16)
60 nokogiri (>= 1.3.3) 73 nokogiri (>= 1.3.3)
61 rack (>= 1.0.0) 74 rack (>= 1.0.0)
62 rack-test (>= 0.5.4) 75 rack-test (>= 0.5.4)
63 selenium-webdriver (~> 2.0) 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 columnize (0.3.6) 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 addressable 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 columnize (>= 0.3.1) 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 bcrypt-ruby (~> 3.0) 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 mail (~> 2.2) 112 mail (~> 2.2)
89 - rspec (~> 2.0)  
90 erubis (2.7.0) 113 erubis (2.7.0)
91 - eventmachine (0.12.10) 114 + eventmachine (1.0.3)
92 execjs (1.4.0) 115 execjs (1.4.0)
93 multi_json (~> 1.0) 116 multi_json (~> 1.0)
94 fabrication (1.3.2) 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 faraday_middleware (0.8.8) 120 faraday_middleware (0.8.8)
98 faraday (>= 0.7.4, < 0.9) 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 happymapper (0.4.0) 131 happymapper (0.4.0)
102 libxml-ruby (~> 2.0) 132 libxml-ruby (~> 2.0)
103 - has_scope (0.5.1)  
104 hashie (1.2.0) 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 httparty 137 httparty
109 hoi (0.0.6) 138 hoi (0.0.6)
110 httparty (> 0.6.0) 139 httparty (> 0.6.0)
@@ -113,152 +142,171 @@ GEM @@ -113,152 +142,171 @@ GEM
113 activesupport 142 activesupport
114 builder 143 builder
115 htmlentities (4.3.1) 144 htmlentities (4.3.1)
116 - httparty (0.9.0) 145 + httparty (0.11.0)
117 multi_json (~> 1.0) 146 multi_json (~> 1.0)
118 - multi_xml  
119 - httpauth (0.1) 147 + multi_xml (>= 0.5.2)
  148 + httpauth (0.2.0)
120 i18n (0.6.1) 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 journey (1.0.4) 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 kaminari (0.14.1) 161 kaminari (0.14.1)
129 actionpack (>= 3.0.0) 162 actionpack (>= 3.0.0)
130 activesupport (>= 3.0.0) 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 addressable (~> 2.3) 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 lighthouse-api (2.0) 169 lighthouse-api (2.0)
139 activeresource (>= 3.0.0) 170 activeresource (>= 3.0.0)
140 activesupport (>= 3.0.0) 171 activesupport (>= 3.0.0)
141 linecache (0.46) 172 linecache (0.46)
142 rbx-require-relative (> 0.0.4) 173 rbx-require-relative (> 0.0.4)
143 - mail (2.4.4)  
144 - i18n (>= 0.4.0) 174 + mail (2.5.4)
145 mime-types (~> 1.16) 175 mime-types (~> 1.16)
146 treetop (~> 1.4.8) 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 tzinfo (~> 0.3.22) 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 bundler (>= 1.0.0) 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 faraday (~> 0.8) 213 faraday (~> 0.8)
173 httpauth (~> 0.1) 214 httpauth (~> 0.1)
174 jwt (~> 0.1.4) 215 jwt (~> 0.1.4)
175 multi_json (~> 1.0) 216 multi_json (~> 1.0)
176 rack (~> 1.2) 217 rack (~> 1.2)
177 - octokit (1.0.7) 218 + octokit (1.18.0)
178 addressable (~> 2.2) 219 addressable (~> 2.2)
179 faraday (~> 0.8) 220 faraday (~> 0.8)
180 faraday_middleware (~> 0.8) 221 faraday_middleware (~> 0.8)
181 hashie (~> 1.2) 222 hashie (~> 1.2)
182 multi_json (~> 1.3) 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 rack 226 rack
186 - omniauth-github (1.0.2) 227 + omniauth-github (1.1.1)
187 omniauth (~> 1.0) 228 omniauth (~> 1.0)
188 omniauth-oauth2 (~> 1.1) 229 omniauth-oauth2 (~> 1.1)
189 omniauth-oauth2 (1.1.1) 230 omniauth-oauth2 (1.1.1)
190 oauth2 (~> 0.8.0) 231 oauth2 (~> 0.8.0)
191 omniauth (~> 1.0) 232 omniauth (~> 1.0)
192 - orm_adapter (0.0.7) 233 + origin (1.1.0)
  234 + orm_adapter (0.4.0)
193 oruen_redmine_client (0.0.1) 235 oruen_redmine_client (0.0.1)
194 activeresource (>= 2.3.0) 236 activeresource (>= 2.3.0)
195 - pivotal-tracker (0.5.4)  
196 - builder 237 + pivotal-tracker (0.5.10)
197 builder 238 builder
198 - happymapper (>= 0.3.2) 239 + crack
199 happymapper (>= 0.3.2) 240 happymapper (>= 0.3.2)
200 nokogiri (>= 1.4.3) 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 rest-client (~> 1.6.0) 244 rest-client (~> 1.6.0)
  245 + pjax_rails (0.3.4)
  246 + jquery-rails
204 polyglot (0.3.3) 247 polyglot (0.3.3)
205 premailer (1.7.3) 248 premailer (1.7.3)
206 css_parser (>= 1.1.9) 249 css_parser (>= 1.1.9)
207 htmlentities (>= 4.0.0) 250 htmlentities (>= 4.0.0)
208 - pry (0.9.9.6) 251 + pry (0.9.12.2)
209 coderay (~> 1.0.5) 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 rack-cache (1.2) 260 rack-cache (1.2)
216 rack (>= 0.4) 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 rack 265 rack
219 - rack-ssl-enforcer (0.2.4) 266 + rack-ssl-enforcer (0.2.5)
220 rack-test (0.6.2) 267 rack-test (0.6.2)
221 rack (>= 1.0) 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 bundler (~> 1.0) 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 rack-ssl (~> 1.3.2) 282 rack-ssl (~> 1.3.2)
236 rake (>= 0.8.7) 283 rake (>= 0.8.7)
237 rdoc (~> 3.4) 284 rdoc (~> 3.4)
238 thor (>= 0.14.6, < 2.0) 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 rbx-require-relative (0.0.9) 288 rbx-require-relative (0.0.9)
242 - rdoc (3.12) 289 + rdoc (3.12.2)
243 json (~> 1.4) 290 json (~> 1.4)
244 - responders (0.9.2)  
245 - railties (~> 3.1) 291 + ref (1.0.5)
246 rest-client (1.6.7) 292 rest-client (1.6.7)
247 mime-types (>= 1.16) 293 mime-types (>= 1.16)
248 ri_cal (0.8.8) 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 actionpack (>= 3.0) 304 actionpack (>= 3.0)
259 activesupport (>= 3.0) 305 activesupport (>= 3.0)
260 railties (>= 3.0) 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 ruby-debug (0.10.4) 310 ruby-debug (0.10.4)
263 columnize (>= 0.1) 311 columnize (>= 0.1)
264 ruby-debug-base (~> 0.10.4.0) 312 ruby-debug-base (~> 0.10.4.0)
@@ -267,52 +315,71 @@ GEM @@ -267,52 +315,71 @@ GEM
267 ruby-fogbugz (0.1.1) 315 ruby-fogbugz (0.1.1)
268 crack 316 crack
269 rubyzip (0.9.9) 317 rubyzip (0.9.9)
270 - rushover (0.1.1) 318 + rushover (0.3.0)
271 json 319 json
272 rest-client 320 rest-client
273 - selenium-webdriver (2.25.0) 321 + safe_yaml (0.9.5)
  322 + selenium-webdriver (2.35.0)
274 childprocess (>= 0.2.5) 323 childprocess (>= 0.2.5)
275 - libwebsocket (~> 0.1.3)  
276 multi_json (~> 1.0) 324 multi_json (~> 1.0)
277 rubyzip 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 hike (~> 1.2) 334 hike (~> 1.2)
  335 + multi_json (~> 1.0)
282 rack (~> 1.0) 336 rack (~> 1.0)
283 tilt (~> 1.1, != 1.3.0) 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 daemons (>= 1.0.9) 353 daemons (>= 1.0.9)
288 eventmachine (>= 0.12.6) 354 eventmachine (>= 0.12.6)
289 rack (>= 1.0.0) 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 polyglot 360 polyglot
295 polyglot (>= 0.3.1) 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 sprockets (>= 2.0.0) 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 execjs (>= 0.3.0) 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 kgio (~> 2.6) 371 kgio (~> 2.6)
306 rack 372 rack
307 raindrops (~> 0.7) 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 rack (>= 1.0) 376 rack (>= 1.0)
311 - webmock (1.8.7) 377 + webmock (1.13.0)
312 addressable (>= 2.2.7) 378 addressable (>= 2.2.7)
313 - crack (>= 0.1.7) 379 + crack (>= 0.3.2)
  380 + websocket (1.0.7)
314 xmpp4r (0.5) 381 xmpp4r (0.5)
315 - xpath (0.1.4) 382 + xpath (1.0.0)
316 nokogiri (~> 1.3) 383 nokogiri (~> 1.3)
317 yajl-ruby (1.1.0) 384 yajl-ruby (1.1.0)
318 385
@@ -321,53 +388,67 @@ PLATFORMS @@ -321,53 +388,67 @@ PLATFORMS
321 388
322 DEPENDENCIES 389 DEPENDENCIES
323 SystemTimer 390 SystemTimer
324 - actionmailer_inline_css (~> 1.3.0) 391 + actionmailer_inline_css
  392 + airbrake
  393 + better_errors
  394 + binding_of_caller
325 bitbucket_rest_api 395 bitbucket_rest_api
326 - bson (= 1.6.2)  
327 - bson_ext (= 1.6.2)  
328 - campy 396 + campy (= 0.1.3)
329 capistrano 397 capistrano
330 - capybara  
331 - database_cleaner (~> 0.6.0) 398 + capybara (~> 2.0.1)
  399 + coveralls
  400 + database_cleaner (~> 0.9.0)
332 debugger 401 debugger
333 - devise (~> 1.5.3) 402 + decent_exposure
  403 + devise
334 email_spec 404 email_spec
335 execjs 405 execjs
336 fabrication (~> 1.3.0) 406 fabrication (~> 1.3.0)
  407 + flowdock
  408 + foreman
  409 + gitlab!
337 haml 410 haml
338 hipchat 411 hipchat
339 hoi 412 hoi
340 hoptoad_notifier (~> 2.4) 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 launchy 419 launchy
345 lighthouse-api 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 omniauth-github 426 omniauth-github
351 oruen_redmine_client 427 oruen_redmine_client
352 pivotal-tracker 428 pivotal-tracker
  429 + pjax_rails
353 pry-rails 430 pry-rails
  431 + quiet_assets
354 rack-ssl 432 rack-ssl
355 rack-ssl-enforcer 433 rack-ssl-enforcer
356 - rails (= 3.2.8)  
357 - rails_autolink (~> 1.0.9) 434 + rails (~> 3.2.13)
  435 + rails_autolink
358 ri_cal 436 ri_cal
359 rspec-rails (~> 2.6) 437 rspec-rails (~> 2.6)
360 ruby-debug 438 ruby-debug
361 ruby-fogbugz 439 ruby-fogbugz
362 rushover 440 rushover
  441 + strong_parameters
  442 + taskmapper (~> 0.8.0)
  443 + taskmapper-unfuddle (~> 0.7.0)
363 therubyracer 444 therubyracer
364 thin 445 thin
365 - timecop 446 + timecop (= 0.6.1)
366 turbo-sprockets-rails3 447 turbo-sprockets-rails3
367 uglifier (>= 1.0.3) 448 uglifier (>= 1.0.3)
368 underscore-rails 449 underscore-rails
369 unicorn 450 unicorn
370 - useragent (~> 0.3.1) 451 + useragent
371 webmock 452 webmock
372 xmpp4r 453 xmpp4r
373 yajl-ruby 454 yajl-ruby
1 -Copyright (c) 2010 Jared Pace 1 +Copyright (c) 2013 errbit team
2 2
3 Permission is hereby granted, free of charge, to any person obtaining 3 Permission is hereby granted, free of charge, to any person obtaining
4 a copy of this software and associated documentation files (the 4 a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 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 \ No newline at end of file 20 \ No newline at end of file
  21 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1 -web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb 1 +web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
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 [travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master 3 [travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master
4 [travis-ci-url]: http://travis-ci.org/errbit/errbit 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 [codeclimate-url]: https://codeclimate.com/github/errbit/errbit 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 ### The open source, self-hosted error catcher 14 ### The open source, self-hosted error catcher
10 15
11 16
12 Errbit is a tool for collecting and managing errors from other applications. 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 so if you are already using Airbrake, you can just point the `airbrake` gem to your Errbit server. 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,9 +61,9 @@ Errbit may be a good fit for you if:
56 * You want to add customer features to your error catcher 61 * You want to add customer features to your error catcher
57 * You're crazy and love managing servers 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 Mailing List 68 Mailing List
64 ------------ 69 ------------
@@ -73,13 +78,19 @@ There is a demo available at [http://errbit-demo.herokuapp.com/](http://errbit-d @@ -73,13 +78,19 @@ There is a demo available at [http://errbit-demo.herokuapp.com/](http://errbit-d
73 Email: demo@errbit-demo.herokuapp.com<br/> 78 Email: demo@errbit-demo.herokuapp.com<br/>
74 Password: password 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 Installation 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 Rails applications. If you're uncomfortable with any step below then Errbit is not 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 **Set up your local box or server(Ubuntu):** 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,7 +98,7 @@ for you. Checkout [Airbrake](http://airbrakeapp.com) from the guys over at
87 98
88 ```bash 99 ```bash
89 apt-get update 100 apt-get update
90 -apt-get install mongodb 101 +apt-get install mongodb-10gen
91 ``` 102 ```
92 103
93 * Install libxml and libcurl 104 * Install libxml and libcurl
@@ -124,21 +135,19 @@ rake errbit:bootstrap @@ -124,21 +135,19 @@ rake errbit:bootstrap
124 script/rails server 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 * Setup server and deploy 143 * Setup server and deploy
137 144
138 ```bash 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 **Deploying to Heroku:** 151 **Deploying to Heroku:**
143 152
144 * Clone the repository 153 * Clone the repository
@@ -146,24 +155,32 @@ cap deploy:setup deploy @@ -146,24 +155,32 @@ cap deploy:setup deploy
146 ```bash 155 ```bash
147 git clone http://github.com/errbit/errbit.git 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 * Create & configure for Heroku 162 * Create & configure for Heroku
151 163
152 ```bash 164 ```bash
153 gem install heroku 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 heroku addons:add sendgrid:starter 170 heroku addons:add sendgrid:starter
157 heroku config:add HEROKU=true 171 heroku config:add HEROKU=true
  172 +heroku config:add SECRET_TOKEN="$(bundle exec rake secret)"
158 heroku config:add ERRBIT_HOST=some-hostname.example.com 173 heroku config:add ERRBIT_HOST=some-hostname.example.com
159 heroku config:add ERRBIT_EMAIL_FROM=example@example.com 174 heroku config:add ERRBIT_EMAIL_FROM=example@example.com
160 git push heroku master 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 ```bash 181 ```bash
166 heroku run rake db:seed 182 heroku run rake db:seed
  183 +heroku run rake db:mongoid:create_indexes
167 ``` 184 ```
168 185
169 * If you are using a free database on Heroku, you may want to periodically clear resolved errors to free up space. 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,6 +216,12 @@ heroku run rake db:seed
199 heroku addons:add deployhooks:http --url="http://YOUR_ERRBIT_HOST/deploys.txt?api_key=YOUR_API_KEY" 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 * Enjoy! 225 * Enjoy!
203 226
204 227
@@ -313,6 +336,7 @@ When upgrading Errbit, please run: @@ -313,6 +336,7 @@ When upgrading Errbit, please run:
313 git pull origin master # assuming origin is the github.com/errbit/errbit repo 336 git pull origin master # assuming origin is the github.com/errbit/errbit repo
314 bundle install 337 bundle install
315 rake db:migrate 338 rake db:migrate
  339 +rake assets:precompile
316 ``` 340 ```
317 341
318 If we change the way that data is stored, this will run any migrations to bring your database up to date. 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,6 +364,20 @@ it will be displayed under the *User Details* tab:
340 364
341 (This tab will be hidden if no user information is available.) 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 Issue Trackers 382 Issue Trackers
345 -------------- 383 --------------
@@ -387,9 +425,35 @@ card_type = Defect, status = Open, priority = Essential @@ -387,9 +425,35 @@ card_type = Defect, status = Open, priority = Essential
387 425
388 * Account is the host of your gitlab installation. i.e. **http://gitlab.example.com** 426 * Account is the host of your gitlab installation. i.e. **http://gitlab.example.com**
389 * To authenticate, Errbit uses token-based authentication. Get your API Key in your user settings (or create special user for this purpose) 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 What if Errbit has an error? 459 What if Errbit has an error?
@@ -434,12 +498,28 @@ Solutions known to work are listed below: @@ -434,12 +498,28 @@ Solutions known to work are listed below:
434 </tr> 498 </tr>
435 </table> 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 TODO 510 TODO
438 ---- 511 ----
439 512
440 * Add ability for watchers to be configured for types of notifications they should receive 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 Special Thanks 523 Special Thanks
444 -------------- 524 --------------
445 525
@@ -448,10 +528,11 @@ Special Thanks @@ -448,10 +528,11 @@ Special Thanks
448 * [Nathan Broadbent (@ndbroadbent)](https://github.com/ndbroadbent) - Maintaining Errbit and contributing many features 528 * [Nathan Broadbent (@ndbroadbent)](https://github.com/ndbroadbent) - Maintaining Errbit and contributing many features
449 * [Vasiliy Ermolovich (@nashby)](https://github.com/nashby) - Contributing and helping to resolve issues and pull requests 529 * [Vasiliy Ermolovich (@nashby)](https://github.com/nashby) - Contributing and helping to resolve issues and pull requests
450 * [Marcin Ciunelis (@martinciu)](https://github.com/martinciu) - Helping to improve Errbit's architecture 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 * [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. 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 Contributing 538 Contributing
@@ -474,10 +555,11 @@ and make **optional** features configurable via `config/config.yml`. @@ -474,10 +555,11 @@ and make **optional** features configurable via `config/config.yml`.
474 * Add tests for it. This is important so we don't break it in a future version unintentionally. 555 * Add tests for it. This is important so we don't break it in a future version unintentionally.
475 * 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) 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 * Send us a pull request. Bonus points for topic branches. 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 Copyright 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 //= require jquery 2 //= require jquery
2 //= require underscore 3 //= require underscore
  4 +// This rails.js version is not like the original.
  5 +// We can't upgrade to the official version
3 //= require rails 6 //= require rails
4 //= require form 7 //= require form
5 //= require jquery.pjax 8 //= require jquery.pjax
app/assets/javascripts/errbit.js
@@ -14,38 +14,28 @@ $(function() { @@ -14,38 +14,28 @@ $(function() {
14 14
15 bindRequiredPasswordMarks(); 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 $('select.choose_other_app').show().focus(); 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 var loc = window.location; 23 var loc = window.location;
33 window.location.href = loc.protocol + "//" + loc.host + loc.pathname + 24 window.location.href = loc.protocol + "//" + loc.host + loc.pathname +
34 "?copy_attributes_from=" + $(this).val(); 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 $(this).closest('form').attr('action', $(this).attr('data-action')); 29 $(this).closest('form').attr('action', $(this).attr('data-action'));
39 }); 30 });
40 31
41 $('.notice-pagination').each(function() { 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 activateTabbedPanels(); 39 activateTabbedPanels();
50 }); 40 });
51 }); 41 });
@@ -83,7 +73,7 @@ $(function() { @@ -83,7 +73,7 @@ $(function() {
83 function toggleProblemsCheckboxes() { 73 function toggleProblemsCheckboxes() {
84 var checkboxToggler = $('#toggle_problems_checkboxes'); 74 var checkboxToggler = $('#toggle_problems_checkboxes');
85 75
86 - checkboxToggler.live("click", function() { 76 + checkboxToggler.on("click", function() {
87 $('input[name^="problems"]').each(function() { 77 $('input[name^="problems"]').each(function() {
88 this.checked = checkboxToggler.get(0).checked; 78 this.checked = checkboxToggler.get(0).checked;
89 }); 79 });
@@ -126,7 +116,7 @@ $(function() { @@ -126,7 +116,7 @@ $(function() {
126 $('td.backtrace_separator').hide(); 116 $('td.backtrace_separator').hide();
127 } 117 }
128 // Show external backtrace lines when clicking separator 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 // Hide external backtrace on page load 120 // Hide external backtrace on page load
131 hide_external_backtrace(); 121 hide_external_backtrace();
132 122
app/assets/javascripts/form.js
@@ -19,7 +19,8 @@ $(function(){ @@ -19,7 +19,8 @@ $(function(){
19 }); 19 });
20 20
21 function activateNestedForms() { 21 function activateNestedForms() {
22 - $('.nested-wrapper').each(function(){ 22 + var wrapper = $('.nested-wrapper')
  23 + wrapper.each(function(){
23 var wrapper = $(this); 24 var wrapper = $(this);
24 25
25 makeNestedItemsDestroyable(wrapper); 26 makeNestedItemsDestroyable(wrapper);
@@ -28,7 +29,7 @@ function activateNestedForms() { @@ -28,7 +29,7 @@ function activateNestedForms() {
28 addLink.click(appendNestedItem); 29 addLink.click(appendNestedItem);
29 wrapper.append(addLink); 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 function makeNestedItemsDestroyable(wrapper) { 35 function makeNestedItemsDestroyable(wrapper) {
@@ -80,7 +81,7 @@ function activateTypeSelector(field_class, section_class) { @@ -80,7 +81,7 @@ function activateTypeSelector(field_class, section_class) {
80 $('div.'+field_class+' > div.'+section_class).not('.chosen').find('input') 81 $('div.'+field_class+' > div.'+section_class).not('.chosen').find('input')
81 .attr('disabled','disabled').val(''); 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 // Look for section in 'data-section', and fall back to 'value' 85 // Look for section in 'data-section', and fall back to 'value'
85 var chosen = $(this).data("section") || $(this).val(); 86 var chosen = $(this).data("section") || $(this).val();
86 var wrapper = $(this).closest('.nested'); 87 var wrapper = $(this).closest('.nested');
app/assets/javascripts/jquery.alerts.js
@@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
12 // $.jAlert( message, [title, callback] ) 12 // $.jAlert( message, [title, callback] )
13 // $.jConfirm( message, [title, callback] ) 13 // $.jConfirm( message, [title, callback] )
14 // $.jPrompt( message, [value, title, callback] ) 14 // $.jPrompt( message, [value, title, callback] )
15 -// 15 +//
16 // History: 16 // History:
17 // 17 //
18 // 1.00 - Released (29 December 2008) 18 // 1.00 - Released (29 December 2008)
@@ -22,16 +22,16 @@ @@ -22,16 +22,16 @@
22 // 1.2 - global methods removed. 22 // 1.2 - global methods removed.
23 // 23 //
24 // License: 24 // License:
25 -// 25 +//
26 // This plugin is dual-licensed under the GNU General Public License and the MIT License and 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 (function($) { 29 (function($) {
30 - 30 +
31 $.alerts = { 31 $.alerts = {
32 - 32 +
33 // These properties can be read/written by accessing $.alerts.propertyName from your scripts at any time 33 // These properties can be read/written by accessing $.alerts.propertyName from your scripts at any time
34 - 34 +
35 verticalOffset: -75, // vertical offset of the dialog from center screen, in pixels 35 verticalOffset: -75, // vertical offset of the dialog from center screen, in pixels
36 horizontalOffset: 0, // horizontal offset of the dialog from center screen, in pixels/ 36 horizontalOffset: 0, // horizontal offset of the dialog from center screen, in pixels/
37 repositionOnResize: true, // re-centers the dialog on window resize 37 repositionOnResize: true, // re-centers the dialog on window resize
@@ -46,37 +46,37 @@ @@ -46,37 +46,37 @@
46 confirm: 'Confirm', 46 confirm: 'Confirm',
47 prompt: 'Prompt' 47 prompt: 'Prompt'
48 }, 48 },
49 - 49 +
50 // Public methods 50 // Public methods
51 - 51 +
52 alert: function(message, title, callback) { 52 alert: function(message, title, callback) {
53 if (! title) title = $.alerts.titles.alert; 53 if (! title) title = $.alerts.titles.alert;
54 $.alerts._show(title, message, null, 'alert', function(result) { 54 $.alerts._show(title, message, null, 'alert', function(result) {
55 if (callback) callback(result); 55 if (callback) callback(result);
56 }); 56 });
57 }, 57 },
58 - 58 +
59 confirm: function(message, title, callback) { 59 confirm: function(message, title, callback) {
60 if (! title) title = $.alerts.titles.confirm; 60 if (! title) title = $.alerts.titles.confirm;
61 $.alerts._show(title, message, null, 'confirm', function(result) { 61 $.alerts._show(title, message, null, 'confirm', function(result) {
62 if (callback) callback(result); 62 if (callback) callback(result);
63 }); 63 });
64 }, 64 },
65 - 65 +
66 prompt: function(message, value, title, callback) { 66 prompt: function(message, value, title, callback) {
67 if (! title) title = $.alerts.titles.prompt; 67 if (! title) title = $.alerts.titles.prompt;
68 $.alerts._show(title, message, value, 'prompt', function(result) { 68 $.alerts._show(title, message, value, 'prompt', function(result) {
69 if(callback) callback(result); 69 if(callback) callback(result);
70 }); 70 });
71 }, 71 },
72 - 72 +
73 // Private methods 73 // Private methods
74 - 74 +
75 _show: function(title, msg, value, type, callback) { 75 _show: function(title, msg, value, type, callback) {
76 - 76 +
77 $.alerts._hide(); 77 $.alerts._hide();
78 $.alerts._overlay('show'); 78 $.alerts._overlay('show');
79 - 79 +
80 $("BODY").append( 80 $("BODY").append(
81 '<div id="popup_container">' + 81 '<div id="popup_container">' +
82 '<h1 id="popup_title"></h1>' + 82 '<h1 id="popup_title"></h1>' +
@@ -84,32 +84,34 @@ @@ -84,32 +84,34 @@
84 '<div id="popup_message"></div>' + 84 '<div id="popup_message"></div>' +
85 '</div>' + 85 '</div>' +
86 '</div>'); 86 '</div>');
87 - 87 +
88 if( $.alerts.dialogClass ) $("#popup_container").addClass($.alerts.dialogClass); 88 if( $.alerts.dialogClass ) $("#popup_container").addClass($.alerts.dialogClass);
89 - 89 +
90 // IE6 Fix 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 $("#popup_container").css({ 95 $("#popup_container").css({
94 position: pos, 96 position: pos,
95 zIndex: 99999, 97 zIndex: 99999,
96 padding: 0, 98 padding: 0,
97 margin: 0 99 margin: 0
98 }); 100 });
99 - 101 +
100 $("#popup_title").text(title); 102 $("#popup_title").text(title);
101 $("#popup_content").addClass(type); 103 $("#popup_content").addClass(type);
102 $("#popup_message").text(msg); 104 $("#popup_message").text(msg);
103 $("#popup_message").html( $("#popup_message").text().replace(/\n/g, '<br />') ); 105 $("#popup_message").html( $("#popup_message").text().replace(/\n/g, '<br />') );
104 - 106 +
105 $("#popup_container").css({ 107 $("#popup_container").css({
106 minWidth: $("#popup_container").outerWidth(), 108 minWidth: $("#popup_container").outerWidth(),
107 maxWidth: $("#popup_container").outerWidth() 109 maxWidth: $("#popup_container").outerWidth()
108 }); 110 });
109 - 111 +
110 $.alerts._reposition(); 112 $.alerts._reposition();
111 $.alerts._maintainPosition(true); 113 $.alerts._maintainPosition(true);
112 - 114 +
113 switch( type ) { 115 switch( type ) {
114 case 'alert': 116 case 'alert':
115 $("#popup_message").after('<div id="popup_panel"><input type="button" value="' + $.alerts.okButton + '" id="popup_ok" /></div>'); 117 $("#popup_message").after('<div id="popup_panel"><input type="button" value="' + $.alerts.okButton + '" id="popup_ok" /></div>');
@@ -158,20 +160,20 @@ @@ -158,20 +160,20 @@
158 break; 160 break;
159 default: break; 161 default: break;
160 } 162 }
161 - 163 +
162 // Make draggable 164 // Make draggable
163 if ($.alerts.draggable && $.fn.draggable) { 165 if ($.alerts.draggable && $.fn.draggable) {
164 $("#popup_container").draggable({ handle: $("#popup_title") }); 166 $("#popup_container").draggable({ handle: $("#popup_title") });
165 $("#popup_title").css({ cursor: 'move' }); 167 $("#popup_title").css({ cursor: 'move' });
166 } 168 }
167 }, 169 },
168 - 170 +
169 _hide: function() { 171 _hide: function() {
170 $("#popup_container").remove(); 172 $("#popup_container").remove();
171 $.alerts._overlay('hide'); 173 $.alerts._overlay('hide');
172 $.alerts._maintainPosition(false); 174 $.alerts._maintainPosition(false);
173 }, 175 },
174 - 176 +
175 _overlay: function(status) { 177 _overlay: function(status) {
176 switch( status ) { 178 switch( status ) {
177 case 'show': 179 case 'show':
@@ -194,23 +196,23 @@ @@ -194,23 +196,23 @@
194 default: break; 196 default: break;
195 } 197 }
196 }, 198 },
197 - 199 +
198 _reposition: function() { 200 _reposition: function() {
199 var top = (($(window).height() / 2) - ($("#popup_container").outerHeight() / 2)) + $.alerts.verticalOffset; 201 var top = (($(window).height() / 2) - ($("#popup_container").outerHeight() / 2)) + $.alerts.verticalOffset;
200 var left = (($(window).width() / 2) - ($("#popup_container").outerWidth() / 2)) + $.alerts.horizontalOffset; 202 var left = (($(window).width() / 2) - ($("#popup_container").outerWidth() / 2)) + $.alerts.horizontalOffset;
201 if( top < 0 ) top = 0; 203 if( top < 0 ) top = 0;
202 if( left < 0 ) left = 0; 204 if( left < 0 ) left = 0;
203 - 205 +
204 // IE6 fix 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 $("#popup_container").css({ 209 $("#popup_container").css({
208 top: top + 'px', 210 top: top + 'px',
209 left: left + 'px' 211 left: left + 'px'
210 }); 212 });
211 $("#popup_overlay").height( $(document).height() ); 213 $("#popup_overlay").height( $(document).height() );
212 }, 214 },
213 - 215 +
214 _maintainPosition: function(status) { 216 _maintainPosition: function(status) {
215 if( $.alerts.repositionOnResize ) { 217 if( $.alerts.repositionOnResize ) {
216 switch(status) { 218 switch(status) {
@@ -224,7 +226,7 @@ @@ -224,7 +226,7 @@
224 } 226 }
225 } 227 }
226 } 228 }
227 - 229 +
228 }; 230 };
229 -  
230 -})(jQuery);  
231 \ No newline at end of file 231 \ No newline at end of file
  232 +
  233 +})(jQuery);
app/assets/javascripts/jquery.js
@@ -1,18 +0,0 @@ @@ -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 \ No newline at end of file 0 \ No newline at end of file
app/assets/javascripts/jquery.pjax.js
@@ -1,264 +0,0 @@ @@ -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 * Unobtrusive scripting adapter for jQuery 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 * https://github.com/rails/jquery-ujs 5 * https://github.com/rails/jquery-ujs
6 6
7 * Uploading file using rails.js 7 * Uploading file using rails.js
@@ -131,11 +131,11 @@ @@ -131,11 +131,11 @@
131 method = element.data('method'); 131 method = element.data('method');
132 url = element.data('url'); 132 url = element.data('url');
133 data = element.serialize(); 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 } else { 135 } else {
136 method = element.data('method'); 136 method = element.data('method');
137 url = element.attr('href'); 137 url = element.attr('href');
138 - data = element.data('params') || null; 138 + data = element.data('params') || null;
139 } 139 }
140 140
141 options = { 141 options = {
app/assets/stylesheets/errbit.css
@@ -304,7 +304,7 @@ form label.inline { display: inline; } @@ -304,7 +304,7 @@ form label.inline { display: inline; }
304 form .checkbox label { display: inline; } 304 form .checkbox label { display: inline; }
305 form .required label { padding-right: 20px; background: transparent url(images/icons/required.png) right 50% no-repeat; } 305 form .required label { padding-right: 20px; background: transparent url(images/icons/required.png) right 50% no-repeat; }
306 form .field_with_errors label { color: #900; } 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 width: 96%; padding: 0.8em; 308 width: 96%; padding: 0.8em;
309 font-size: 1em; 309 font-size: 1em;
310 color: #787878; border: 1px solid #C6C6C6; 310 color: #787878; border: 1px solid #C6C6C6;
@@ -316,7 +316,7 @@ form textarea { @@ -316,7 +316,7 @@ form textarea {
316 } 316 }
317 form textarea.short { height: 8em; } 317 form textarea.short { height: 8em; }
318 form textarea.supershort { height: 4em; } 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 box-shadow: 0px 0px 4px #69C; 320 box-shadow: 0px 0px 4px #69C;
321 -moz-box-shadow: 0px 0px 4px #69C; 321 -moz-box-shadow: 0px 0px 4px #69C;
322 -webkit-box-shadow: 0px 0px 4px #69C 322 -webkit-box-shadow: 0px 0px 4px #69C
@@ -654,7 +654,6 @@ table.errs td.message a { @@ -654,7 +654,6 @@ table.errs td.message a {
654 overflow: hidden; 654 overflow: hidden;
655 text-overflow: ellipsis; 655 text-overflow: ellipsis;
656 -o-text-overflow: ellipsis; 656 -o-text-overflow: ellipsis;
657 - white-space: nowrap;  
658 /* ------ */ 657 /* ------ */
659 } 658 }
660 table.errs td.message em { 659 table.errs td.message em {
@@ -714,7 +713,9 @@ table.deploys td.when { @@ -714,7 +713,9 @@ table.deploys td.when {
714 .notice-pagination-loader { 713 .notice-pagination-loader {
715 visibility: hidden; 714 visibility: hidden;
716 float: left; 715 float: left;
717 - margin-right: 2em; 716 + width: 16px;
  717 + height: 16px;
  718 + margin-right: 1em;
718 } 719 }
719 .notice-pagination-loader img { 720 .notice-pagination-loader img {
720 vertical-align: middle 721 vertical-align: middle
app/controllers/api/v1/notices_controller.rb
1 class Api::V1::NoticesController < ApplicationController 1 class Api::V1::NoticesController < ApplicationController
2 respond_to :json, :xml 2 respond_to :json, :xml
3 - 3 +
4 def index 4 def index
5 query = {} 5 query = {}
6 fields = %w{created_at message error_class} 6 fields = %w{created_at message error_class}
7 - 7 +
8 if params.key?(:start_date) && params.key?(:end_date) 8 if params.key?(:start_date) && params.key?(:end_date)
9 start_date = Time.parse(params[:start_date]).utc 9 start_date = Time.parse(params[:start_date]).utc
10 end_date = Time.parse(params[:end_date]).utc 10 end_date = Time.parse(params[:end_date]).utc
11 query = {:created_at => {"$lte" => end_date, "$gte" => start_date}} 11 query = {:created_at => {"$lte" => end_date, "$gte" => start_date}}
12 end 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 respond_to do |format| 18 respond_to do |format|
17 format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path 19 format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
18 format.json { render :json => Yajl.dump(results) } 20 format.json { render :json => Yajl.dump(results) }
19 format.xml { render :xml => results } 21 format.xml { render :xml => results }
20 end 22 end
21 end 23 end
22 - 24 +
23 end 25 end
app/controllers/api/v1/problems_controller.rb
1 class Api::V1::ProblemsController < ApplicationController 1 class Api::V1::ProblemsController < ApplicationController
2 respond_to :json, :xml 2 respond_to :json, :xml
3 - 3 +
4 def index 4 def index
5 query = {} 5 query = {}
6 fields = %w{app_id app_name environment message where first_notice_at last_notice_at resolved resolved_at notices_count} 6 fields = %w{app_id app_name environment message where first_notice_at last_notice_at resolved resolved_at notices_count}
7 - 7 +
8 if params.key?(:start_date) && params.key?(:end_date) 8 if params.key?(:start_date) && params.key?(:end_date)
9 start_date = Time.parse(params[:start_date]).utc 9 start_date = Time.parse(params[:start_date]).utc
10 end_date = Time.parse(params[:end_date]).utc 10 end_date = Time.parse(params[:end_date]).utc
11 query = {:first_notice_at=>{"$lte"=>end_date}, "$or"=>[{:resolved_at=>nil}, {:resolved_at=>{"$gte"=>start_date}}]} 11 query = {:first_notice_at=>{"$lte"=>end_date}, "$or"=>[{:resolved_at=>nil}, {:resolved_at=>{"$gte"=>start_date}}]}
12 end 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 respond_to do |format| 18 respond_to do |format|
17 format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path 19 format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
18 format.json { render :json => Yajl.dump(results) } 20 format.json { render :json => Yajl.dump(results) }
19 format.xml { render :xml => results } 21 format.xml { render :xml => results }
20 end 22 end
21 end 23 end
22 - 24 +
23 end 25 end
app/controllers/api/v1/stats_controller.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -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,12 +13,28 @@ class ApplicationController &lt; ActionController::Base
13 13
14 rescue_from ActionController::RedirectBackError, :with => :redirect_to_root 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 protected 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 def require_admin! 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 end 38 end
23 39
24 def redirect_to_root 40 def redirect_to_root
@@ -30,4 +46,3 @@ protected @@ -30,4 +46,3 @@ protected
30 end 46 end
31 47
32 end 48 end
33 -  
app/controllers/apps_controller.rb
1 -class AppsController < InheritedResources::Base 1 +class AppsController < ApplicationController
  2 +
  3 + include ProblemsSearcher
  4 +
2 before_filter :require_admin!, :except => [:index, :show] 5 before_filter :require_admin!, :except => [:index, :show]
3 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update] 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 respond_to :html 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 end 46 end
28 47
29 def create 48 def create
30 - @app = App.new(params[:app])  
31 initialize_subclassed_issue_tracker 49 initialize_subclassed_issue_tracker
32 initialize_subclassed_notification_service 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 end 57 end
35 58
36 def update 59 def update
37 - @app = resource  
38 initialize_subclassed_issue_tracker 60 initialize_subclassed_issue_tracker
39 initialize_subclassed_notification_service 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 end 68 end
42 69
43 - def new  
44 - plug_params(build_resource)  
45 - new! 70 + def edit
  71 + plug_params(app)
46 end 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 end 81 end
52 82
53 protected 83 protected
54 - def collection  
55 - @apps ||= end_of_association_chain.all.sort  
56 - end  
57 84
58 def initialize_subclassed_issue_tracker 85 def initialize_subclassed_issue_tracker
59 # set the app's issue tracker 86 # set the app's issue tracker
60 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type] 87 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type]
61 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type) 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 end 90 end
64 end 91 end
65 end 92 end
@@ -68,21 +95,11 @@ class AppsController &lt; InheritedResources::Base @@ -68,21 +95,11 @@ class AppsController &lt; InheritedResources::Base
68 # set the app's notification service 95 # set the app's notification service
69 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type] 96 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
70 if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type) 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 end 99 end
73 end 100 end
74 end 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 def plug_params app 103 def plug_params app
87 app.watchers.build if app.watchers.none? 104 app.watchers.build if app.watchers.none?
88 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured? 105 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured?
@@ -105,5 +122,20 @@ class AppsController &lt; InheritedResources::Base @@ -105,5 +122,20 @@ class AppsController &lt; InheritedResources::Base
105 end 122 end
106 end 123 end
107 end 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 end 140 end
109 141
app/controllers/notices_controller.rb
1 class NoticesController < ApplicationController 1 class NoticesController < ApplicationController
2 - respond_to :xml 2 +
  3 + class ParamsError < StandardError; end
3 4
4 skip_before_filter :authenticate_user!, :only => :create 5 skip_before_filter :authenticate_user!, :only => :create
5 6
  7 + rescue_from ParamsError, :with => :bad_params
  8 +
6 def create 9 def create
7 # params[:data] if the notice came from a GET request, raw_post if it came via POST 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 end 21 end
12 - render :xml => api_xml  
13 end 22 end
14 23
15 # Redirects a notice to the problem page. Useful when using User Information at Airbrake gem. 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,4 +26,20 @@ class NoticesController &lt; ApplicationController
17 problem = Notice.find(params[:id]).problem 26 problem = Notice.find(params[:id]).problem
18 redirect_to app_problem_path(problem.app, problem) 27 redirect_to app_problem_path(problem.app, problem)
19 end 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 end 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 class ProblemsController < ApplicationController 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 end 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 def show 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 @notice = @notices.first 60 @notice = @notices.first
30 @comment = Comment.new 61 @comment = Comment.new
31 - if request.headers['X-PJAX']  
32 - params["_pjax"] = nil  
33 - render :layout => false  
34 - end  
35 end 62 end
36 63
37 def create_issue 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 unless issue_creation.execute 68 unless issue_creation.execute
41 - flash[:error] = issue_creation.errors[:base].first 69 + flash[:error] = issue_creation.errors.full_messages.join(', ')
42 end 70 end
43 71
44 - redirect_to app_problem_path(@app, @problem) 72 + redirect_to app_problem_path(app, problem)
45 end 73 end
46 74
47 def unlink_issue 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 end 78 end
51 79
52 def resolve 80 def resolve
53 - @problem.resolve! 81 + problem.resolve!
54 flash[:success] = 'Great news everyone! The err has been resolved.' 82 flash[:success] = 'Great news everyone! The err has been resolved.'
55 redirect_to :back 83 redirect_to :back
56 rescue ActionController::RedirectBackError 84 rescue ActionController::RedirectBackError
57 - redirect_to app_path(@app) 85 + redirect_to app_path(app)
58 end 86 end
59 87
60 def resolve_several 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 redirect_to :back 91 redirect_to :back
64 end 92 end
65 93
66 def unresolve_several 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 redirect_to :back 97 redirect_to :back
70 end 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 def merge_several 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 else 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 end 111 end
79 redirect_to :back 112 redirect_to :back
80 end 113 end
81 114
82 def unmerge_several 115 def unmerge_several
83 - all = @selected_problems.map(&:unmerge!).flatten 116 + all = selected_problems.map(&:unmerge!).flatten
84 flash[:success] = "#{I18n.t(:n_errs_have, :count => all.length)} been unmerged." 117 flash[:success] = "#{I18n.t(:n_errs_have, :count => all.length)} been unmerged."
85 redirect_to :back 118 redirect_to :back
86 end 119 end
87 120
88 def destroy_several 121 def destroy_several
89 - nb_problem_destroy = ProblemDestroy.execute(@selected_problems) 122 + nb_problem_destroy = ProblemDestroy.execute(selected_problems)
90 flash[:notice] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted." 123 flash[:notice] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted."
91 redirect_to :back 124 redirect_to :back
92 end 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 end 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 end 146 end
  147 + end
128 end 148 end
129 149
app/controllers/problems_searcher.rb 0 → 100644
@@ -0,0 +1,34 @@ @@ -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,71 +2,76 @@ class UsersController &lt; ApplicationController
2 respond_to :html 2 respond_to :html
3 3
4 before_filter :require_admin!, :except => [:edit, :update] 4 before_filter :require_admin!, :except => [:edit, :update]
5 - before_filter :find_user, :only => [:show, :edit, :update, :destroy, :unlink_github]  
6 before_filter :require_user_edit_priviledges, :only => [:edit, :update] 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 def create 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 else 20 else
26 render :new 21 render :new
27 end 22 end
28 end 23 end
29 24
30 def update 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 else 29 else
44 render :edit 30 render :edit
45 end 31 end
46 end 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 def destroy 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 redirect_to users_path 46 redirect_to users_path
53 end 47 end
54 48
55 def unlink_github 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 end 52 end
59 53
60 protected 54 protected
61 55
62 - def find_user  
63 - @user = User.find(params[:id])  
64 - end  
65 -  
66 def require_user_edit_priviledges 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 redirect_to(root_path) and return(false) unless can_edit 58 redirect_to(root_path) and return(false) unless can_edit
69 end 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 end 76 end
72 77
app/helpers/application_helper.rb
@@ -8,11 +8,11 @@ module ApplicationHelper @@ -8,11 +8,11 @@ module ApplicationHelper
8 notices.each_with_index do |notice,idx| 8 notices.each_with_index do |notice,idx|
9 cal.event do |event| 9 cal.event do |event|
10 event.summary = "#{idx+1} #{notice.message.to_s}" 10 event.summary = "#{idx+1} #{notice.message.to_s}"
11 - event.description = notice.request['url'] 11 + event.description = notice.url if notice.url
12 event.dtstart = notice.created_at.utc 12 event.dtstart = notice.created_at.utc
13 event.dtend = notice.created_at.utc + 60.minutes 13 event.dtend = notice.created_at.utc + 60.minutes
14 event.organizer = notice.server_environment && notice.server_environment["hostname"] 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 event.url = app_problem_url(:app_id => notice.problem.app.id, :id => notice.problem) 16 event.url = app_problem_url(:app_id => notice.problem.app.id, :id => notice.problem)
17 end 17 end
18 end 18 end
@@ -57,7 +57,7 @@ module ApplicationHelper @@ -57,7 +57,7 @@ module ApplicationHelper
57 total = (options[:total] || total_from_tallies(tallies)) 57 total = (options[:total] || total_from_tallies(tallies))
58 percent = 100.0 / total.to_f 58 percent = 100.0 / total.to_f
59 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ 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 render "problems/tally_table", :rows => rows 61 render "problems/tally_table", :rows => rows
62 end 62 end
63 63
app/helpers/apps_helper.rb
@@ -4,7 +4,7 @@ module AppsHelper @@ -4,7 +4,7 @@ module AppsHelper
4 html = link_to('copy settings from another app', '#', 4 html = link_to('copy settings from another app', '#',
5 :class => 'button copy_config') 5 :class => 'button copy_config')
6 html << select("duplicate", "app", 6 html << select("duplicate", "app",
7 - App.all.reject{|a| a == @app }. 7 + App.all.asc(:name).reject{|a| a == @app }.
8 collect{|p| [ p.name, p.id ] }, {:include_blank => "[choose app]"}, 8 collect{|p| [ p.name, p.id ] }, {:include_blank => "[choose app]"},
9 {:class => "choose_other_app", :style => "display: none;"}) 9 {:class => "choose_other_app", :style => "display: none;"})
10 return html 10 return html
@@ -41,7 +41,7 @@ module AppsHelper @@ -41,7 +41,7 @@ module AppsHelper
41 def detect_any_apps_with_attributes 41 def detect_any_apps_with_attributes
42 @any_github_repos = @any_issue_trackers = @any_deploys = @any_bitbucket_repos = @any_notification_services = false 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 @any_github_repos ||= app.github_repo? 45 @any_github_repos ||= app.github_repo?
46 @any_bitbucket_repos ||= app.bitbucket_repo? 46 @any_bitbucket_repos ||= app.bitbucket_repo?
47 @any_issue_trackers ||= app.issue_tracker_configured? 47 @any_issue_trackers ||= app.issue_tracker_configured?
app/helpers/backtrace_line_helper.rb
1 module BacktraceLineHelper 1 module BacktraceLineHelper
2 def link_to_source_file(line, &block) 2 def link_to_source_file(line, &block)
3 text = capture_haml(&block) 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 end 5 end
6 6
7 private 7 private
8 def link_to_in_app_source_file(line, text) 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 end 16 end
11 17
12 def link_to_repo_source_file(line, text) 18 def link_to_repo_source_file(line, text)
13 link_to_github(line, text) || link_to_bitbucket(line, text) 19 link_to_github(line, text) || link_to_bitbucket(line, text)
14 end 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 def link_to_external_source_file(text) 28 def link_to_external_source_file(text)
17 text 29 text
18 end 30 end
@@ -31,7 +43,7 @@ module BacktraceLineHelper @@ -31,7 +43,7 @@ module BacktraceLineHelper
31 43
32 def link_to_issue_tracker_file(line, text = nil) 44 def link_to_issue_tracker_file(line, text = nil)
33 return unless line.app.issue_tracker && line.app.issue_tracker.respond_to?(:url_to_file) 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 link_to(text || line.file_name, href, :target => '_blank') 47 link_to(text || line.file_name, href, :target => '_blank')
36 end 48 end
37 49
app/helpers/problems_helper.rb
@@ -11,17 +11,22 @@ module ProblemsHelper @@ -11,17 +11,22 @@ module ProblemsHelper
11 end 11 end
12 12
13 def gravatar_tag(email, options = {}) 13 def gravatar_tag(email, options = {})
  14 + return nil unless email.present?
  15 +
14 image_tag gravatar_url(email, options), :alt => email, :class => 'gravatar' 16 image_tag gravatar_url(email, options), :alt => email, :class => 'gravatar'
15 end 17 end
16 18
17 def gravatar_url(email, options = {}) 19 def gravatar_url(email, options = {})
  20 + return nil unless email.present?
  21 +
18 default_options = { 22 default_options = {
19 :d => Errbit::Config.gravatar_default, 23 :d => Errbit::Config.gravatar_default,
20 } 24 }
21 options.reverse_merge! default_options 25 options.reverse_merge! default_options
22 params = options.extract!(:s, :d).delete_if { |k, v| v.blank? } 26 params = options.extract!(:s, :d).delete_if { |k, v| v.blank? }
23 email_hash = Digest::MD5.hexdigest(email) 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 end 30 end
26 end 31 end
27 32
app/helpers/sort_helper.rb
1 # encoding: utf-8 1 # encoding: utf-8
2 module SortHelper 2 module SortHelper
3 - 3 +
4 def link_for_sort(name, field=nil) 4 def link_for_sort(name, field=nil)
5 field ||= name.underscore 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 url = request.path + "?sort=#{field}&order=#{order}" 8 url = request.path + "?sort=#{field}&order=#{order}"
9 options = {} 9 options = {}
10 options.merge!(:class => "current #{order}") if current 10 options.merge!(:class => "current #{order}") if current
11 link_to(name, url, options) 11 link_to(name, url, options)
12 end 12 end
13 - 13 +
14 end 14 end
app/interactors/issue_creation.rb
@@ -41,15 +41,11 @@ class IssueCreation @@ -41,15 +41,11 @@ class IssueCreation
41 end 41 end
42 42
43 def execute 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 errors.empty? 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 end 50 end
55 end 51 end
app/interactors/problem_destroy.rb
@@ -37,12 +37,12 @@ class ProblemDestroy @@ -37,12 +37,12 @@ class ProblemDestroy
37 end 37 end
38 38
39 def delete_errs 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 end 42 end
43 43
44 def delete_comments 44 def delete_comments
45 - Comment.collection.remove(:_id => { '$in' => comments_id }) 45 + Comment.delete_all(:_id => { '$in' => comments_id })
46 end 46 end
47 47
48 end 48 end
app/interactors/problem_merge.rb 0 → 100644
@@ -0,0 +1,28 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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,14 +3,20 @@
3 require Rails.root.join('config/routes.rb') 3 require Rails.root.join('config/routes.rb')
4 4
5 class Mailer < ActionMailer::Base 5 class Mailer < ActionMailer::Base
  6 + helper ApplicationHelper
  7 + helper BacktraceLineHelper
  8 +
6 default :from => Errbit::Config.email_from 9 default :from => Errbit::Config.email_from
7 10
8 def err_notification(notice) 11 def err_notification(notice)
9 @notice = notice 12 @notice = notice
10 @app = notice.app 13 @app = notice.app
11 14
  15 + count = @notice.similar_count
  16 + count = count > 1 ? "(#{count}) " : ""
  17 +
12 mail :to => @app.notification_recipients, 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 end 20 end
15 21
16 def deploy_notification(deploy) 22 def deploy_notification(deploy)
@@ -20,5 +26,17 @@ class Mailer &lt; ActionMailer::Base @@ -20,5 +26,17 @@ class Mailer &lt; ActionMailer::Base
20 mail :to => @app.notification_recipients, 26 mail :to => @app.notification_recipients,
21 :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}" 27 :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}"
22 end 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 class App 1 class App
  2 + include Comparable
2 include Mongoid::Document 3 include Mongoid::Document
3 include Mongoid::Timestamps 4 include Mongoid::Timestamps
4 - include Comparable  
5 5
6 field :name, :type => String 6 field :name, :type => String
7 field :api_key 7 field :api_key
8 field :github_repo 8 field :github_repo
9 field :bitbucket_repo 9 field :bitbucket_repo
  10 + field :asset_host
10 field :repository_branch 11 field :repository_branch
11 field :resolve_errs_on_deploy, :type => Boolean, :default => false 12 field :resolve_errs_on_deploy, :type => Boolean, :default => false
12 field :notify_all_users, :type => Boolean, :default => false 13 field :notify_all_users, :type => Boolean, :default => false
@@ -15,7 +16,12 @@ class App @@ -15,7 +16,12 @@ class App
15 field :email_at_notices, :type => Array, :default => Errbit::Config.email_at_notices 16 field :email_at_notices, :type => Array, :default => Errbit::Config.email_at_notices
16 17
17 # Some legacy apps may have string as key instead of BSON::ObjectID 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 embeds_many :watchers 26 embeds_many :watchers
21 embeds_many :deploys 27 embeds_many :deploys
@@ -41,46 +47,17 @@ class App @@ -41,46 +47,17 @@ class App
41 accepts_nested_attributes_for :notification_service, :allow_destroy => true, 47 accepts_nested_attributes_for :notification_service, :allow_destroy => true,
42 :reject_if => proc { |attrs| !NotificationService.subclasses.map(&:to_s).include?(attrs[:type].to_s) } 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 def find_or_create_err!(attrs) 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 end 61 end
85 62
86 # Mongoid Bug: find(id) on association proxies returns an Enumerator 63 # Mongoid Bug: find(id) on association proxies returns an Enumerator
@@ -89,7 +66,7 @@ class App @@ -89,7 +66,7 @@ class App
89 end 66 end
90 67
91 def self.find_by_api_key!(key) 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 end 70 end
94 71
95 def last_deploy_at 72 def last_deploy_at
@@ -103,7 +80,7 @@ class App @@ -103,7 +80,7 @@ class App
103 end 80 end
104 alias :notify_on_errs? :notify_on_errs 81 alias :notify_on_errs? :notify_on_errs
105 82
106 - def notifiable? 83 + def emailable?
107 notify_on_errs? && notification_recipients.any? 84 notify_on_errs? && notification_recipients.any?
108 end 85 end
109 86
@@ -125,7 +102,7 @@ class App @@ -125,7 +102,7 @@ class App
125 end 102 end
126 103
127 def github_url_to_file(file) 104 def github_url_to_file(file)
128 - "#{github_url}/blob/#{repo_branch + file}" 105 + "#{github_url}/blob/#{repo_branch}/#{file}"
129 end 106 end
130 107
131 def bitbucket_repo? 108 def bitbucket_repo?
@@ -137,7 +114,7 @@ class App @@ -137,7 +114,7 @@ class App
137 end 114 end
138 115
139 def bitbucket_url_to_file(file) 116 def bitbucket_url_to_file(file)
140 - "#{bitbucket_url}/src/#{repo_branch + file}" 117 + "#{bitbucket_url}/src/#{repo_branch}/#{file}"
141 end 118 end
142 119
143 120
app/models/backtrace.rb
@@ -3,7 +3,7 @@ class Backtrace @@ -3,7 +3,7 @@ class Backtrace
3 include Mongoid::Timestamps 3 include Mongoid::Timestamps
4 4
5 field :fingerprint 5 field :fingerprint
6 - index :fingerprint 6 + index :fingerprint => 1
7 7
8 has_many :notices 8 has_many :notices
9 has_one :notice 9 has_one :notice
@@ -19,11 +19,11 @@ class Backtrace @@ -19,11 +19,11 @@ class Backtrace
19 end 19 end
20 20
21 def similar 21 def similar
22 - Backtrace.first(:conditions => { :fingerprint => fingerprint } ) 22 + Backtrace.find_by(:fingerprint => fingerprint) rescue nil
23 end 23 end
24 24
25 def raw=(raw) 25 def raw=(raw)
26 - raw.each do |raw_line| 26 + raw.compact.each do |raw_line|
27 lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call) 27 lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call)
28 end 28 end
29 end 29 end
app/models/backtrace_line.rb
1 class BacktraceLine 1 class BacktraceLine
2 include Mongoid::Document 2 include Mongoid::Document
3 - IN_APP_PATH = %r{^\[PROJECT_ROOT\]\/(?!(vendor))} 3 + IN_APP_PATH = %r{^\[PROJECT_ROOT\](?!(\/vendor))/?}
4 GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)} 4 GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)}
5 5
6 field :number, :type => Integer 6 field :number, :type => Integer
  7 + field :column, :type => Integer
7 field :file 8 field :file
8 field :method 9 field :method
9 10
@@ -14,7 +15,7 @@ class BacktraceLine @@ -14,7 +15,7 @@ class BacktraceLine
14 delegate :app, :to => :backtrace 15 delegate :app, :to => :backtrace
15 16
16 def to_s 17 def to_s
17 - "#{file}:#{number}" 18 + "#{file_relative}:#{number}" << (column.present? ? ":#{column}" : "")
18 end 19 end
19 20
20 def in_app? 21 def in_app?
app/models/backtrace_line_normalizer.rb
1 class BacktraceLineNormalizer 1 class BacktraceLineNormalizer
2 def initialize(raw_line) 2 def initialize(raw_line)
3 - @raw_line = raw_line 3 + @raw_line = raw_line || {}
4 end 4 end
5 5
6 def call 6 def call
@@ -9,11 +9,24 @@ class BacktraceLineNormalizer @@ -9,11 +9,24 @@ class BacktraceLineNormalizer
9 9
10 private 10 private
11 def normalized_file 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 end 22 end
14 23
15 def normalized_method 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 end 30 end
18 31
19 end 32 end
app/models/comment.rb
@@ -6,13 +6,22 @@ class Comment @@ -6,13 +6,22 @@ class Comment
6 before_destroy :decrease_counter_cache 6 before_destroy :decrease_counter_cache
7 7
8 field :body, :type => String 8 field :body, :type => String
9 - index :user_id 9 + index(:user_id => 1)
10 10
11 belongs_to :err, :class_name => "Problem" 11 belongs_to :err, :class_name => "Problem"
12 belongs_to :user 12 belongs_to :user
  13 + delegate :app, :to => :err
13 14
14 validates_presence_of :body 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 protected 25 protected
17 def increase_counter_cache 26 def increase_counter_cache
18 err.inc(:comments_count, 1) 27 err.inc(:comments_count, 1)
@@ -23,4 +32,3 @@ class Comment @@ -23,4 +32,3 @@ class Comment
23 end 32 end
24 33
25 end 34 end
26 -  
app/models/comment_observer.rb 0 → 100644
@@ -0,0 +1,8 @@ @@ -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,7 +8,7 @@ class Deploy
8 field :revision 8 field :revision
9 field :message 9 field :message
10 10
11 - index :created_at, Mongo::DESCENDING 11 + index(:created_at => -1)
12 12
13 embedded_in :app, :inverse_of => :deploys 13 embedded_in :app, :inverse_of => :deploys
14 14
app/models/err.rb
@@ -6,20 +6,16 @@ class Err @@ -6,20 +6,16 @@ class Err
6 include Mongoid::Document 6 include Mongoid::Document
7 include Mongoid::Timestamps 7 include Mongoid::Timestamps
8 8
9 - field :error_class, :default => "UnknownError"  
10 - field :component  
11 - field :action  
12 - field :environment, :default => "unknown"  
13 field :fingerprint 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 has_many :notices, :inverse_of => :err, :dependent => :destroy 15 has_many :notices, :inverse_of => :err, :dependent => :destroy
21 16
  17 + validates_presence_of :problem_id, :fingerprint
  18 +
22 delegate :app, :resolved?, :to => :problem 19 delegate :app, :resolved?, :to => :problem
23 20
24 end 21 end
25 -  
app/models/error_report.rb
1 -require 'digest/sha1'  
2 require 'hoptoad_notifier' 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 class ErrorReport 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 def initialize(xml_or_attributes) 20 def initialize(xml_or_attributes)
8 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access 21 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access
9 @attributes.each{|k, v| instance_variable_set(:"@#{k}", v) } 22 @attributes.each{|k, v| instance_variable_set(:"@#{k}", v) }
10 end 23 end
11 24
12 - def fingerprint  
13 - @fingerprint ||= Digest::SHA1.hexdigest(fingerprint_source.to_s)  
14 - end  
15 -  
16 def rails_env 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 end 29 end
27 30
28 def app 31 def app
29 - @app ||= App.find_by_api_key!(api_key) 32 + @app ||= App.where(:api_key => api_key).first
30 end 33 end
31 34
32 def backtrace 35 def backtrace
@@ -34,7 +37,9 @@ class ErrorReport @@ -34,7 +37,9 @@ class ErrorReport
34 end 37 end
35 38
36 def generate_notice! 39 def generate_notice!
37 - notice = Notice.new( 40 + return unless valid?
  41 + return @notice if @notice
  42 + @notice = Notice.new(
38 :message => message, 43 :message => message,
39 :error_class => error_class, 44 :error_class => error_class,
40 :backtrace_id => backtrace.id, 45 :backtrace_id => backtrace.id,
@@ -42,31 +47,35 @@ class ErrorReport @@ -42,31 +47,35 @@ class ErrorReport
42 :server_environment => server_environment, 47 :server_environment => server_environment,
43 :notifier => notifier, 48 :notifier => notifier,
44 :user_attributes => user_attributes, 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 :error_class => error_class, 65 :error_class => error_class,
50 - :component => component,  
51 - :action => action,  
52 :environment => rails_env, 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 end 73 end
58 74
59 private 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 end 79 end
70 80
71 end 81 end
72 -  
app/models/fingerprint.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -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,6 +15,15 @@ class IssueTracker
15 field :password, :type => String 15 field :password, :type => String
16 field :ticket_properties, :type => String 16 field :ticket_properties, :type => String
17 field :subdomain, :type => String 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 validate :check_params 28 validate :check_params
20 29
@@ -39,5 +48,15 @@ class IssueTracker @@ -39,5 +48,15 @@ class IssueTracker
39 def configured? 48 def configured?
40 project_id.present? 49 project_id.present?
41 end 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 end 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 end 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 end 54 end
46 end 55 end
47 -  
app/models/issue_trackers/gitlab_tracker.rb
@@ -10,7 +10,7 @@ if defined? Gitlab @@ -10,7 +10,7 @@ if defined? Gitlab
10 :placeholder => "API Token for your account" 10 :placeholder => "API Token for your account"
11 }], 11 }],
12 [:project_id, { 12 [:project_id, {
13 - :label => "Ticket Project Short Name / ID", 13 + :label => "Ticket Project ID (use Number)",
14 :placeholder => "Gitlab Project where issues will be created" 14 :placeholder => "Gitlab Project where issues will be created"
15 }] 15 }]
16 ] 16 ]
@@ -23,19 +23,25 @@ if defined? Gitlab @@ -23,19 +23,25 @@ if defined? Gitlab
23 23
24 def create_issue(problem, reported_by = nil) 24 def create_issue(problem, reported_by = nil)
25 Gitlab.configure do |config| 25 Gitlab.configure do |config|
26 - config.endpoint = "#{account}/api/v2" 26 + config.endpoint = "#{account}/api/v3"
27 config.private_token = api_token 27 config.private_token = api_token
28 config.user_agent = 'Errbit User Agent' 28 config.user_agent = 'Errbit User Agent'
29 end 29 end
30 title = issue_title problem 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 end 39 end
34 40
35 def body_template 41 def body_template
36 @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/gitlab_body.txt.erb").gsub(/^\s*/, '')) 42 @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/gitlab_body.txt.erb").gsub(/^\s*/, ''))
37 end 43 end
38 - 44 +
39 def url 45 def url
40 "#{account}/#{project_id}/issues" 46 "#{account}/#{project_id}/issues"
41 end 47 end
app/models/issue_trackers/jira_tracker.rb 0 → 100644
@@ -0,0 +1,110 @@ @@ -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 \ No newline at end of file 111 \ No newline at end of file
app/models/issue_trackers/redmine_tracker.rb
@@ -9,6 +9,12 @@ if defined? RedmineClient @@ -9,6 +9,12 @@ if defined? RedmineClient
9 [:api_token, { 9 [:api_token, {
10 :placeholder => "API Token for your account" 10 :placeholder => "API Token for your account"
11 }], 11 }],
  12 + [:username, {
  13 + :placeholder => "Your username"
  14 + }],
  15 + [:password, {
  16 + :placeholder => "Your password"
  17 + }],
12 [:project_id, { 18 [:project_id, {
13 :label => "Ticket Project", 19 :label => "Ticket Project",
14 :placeholder => "Redmine Project where tickets will be created" 20 :placeholder => "Redmine Project where tickets will be created"
@@ -22,15 +28,19 @@ if defined? RedmineClient @@ -22,15 +28,19 @@ if defined? RedmineClient
22 28
23 def check_params 29 def check_params
24 if Fields.detect {|f| self[f[0]].blank? && !f[1][:optional]} 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 end 32 end
27 end 33 end
28 34
29 def create_issue(problem, reported_by = nil) 35 def create_issue(problem, reported_by = nil)
30 token = api_token 36 token = api_token
31 acc = account 37 acc = account
  38 + user = username
  39 + passwd = password
32 RedmineClient::Base.configure do 40 RedmineClient::Base.configure do
33 self.token = token 41 self.token = token
  42 + self.user = user
  43 + self.password = passwd
34 self.site = acc 44 self.site = acc
35 self.format = :xml 45 self.format = :xml
36 end 46 end
@@ -47,17 +57,18 @@ if defined? RedmineClient @@ -47,17 +57,18 @@ if defined? RedmineClient
47 def url_to_file(file_path, line_number = nil) 57 def url_to_file(file_path, line_number = nil)
48 # alt_project_id let's users specify a different project for tickets / app files. 58 # alt_project_id let's users specify a different project for tickets / app files.
49 project = self.alt_project_id.present? ? self.alt_project_id : self.project_id 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 line_number ? url << "#L#{line_number}" : url 61 line_number ? url << "#L#{line_number}" : url
52 end 62 end
53 63
54 def body_template 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 end 66 end
57 67
58 def url 68 def url
59 acc_url = account.start_with?('http') ? account : "http://#{account}" 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 rescue URI::InvalidURIError 72 rescue URI::InvalidURIError
62 end 73 end
63 end 74 end
app/models/issue_trackers/unfuddle_tracker.rb 0 → 100644
@@ -0,0 +1,69 @@ @@ -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 require 'recurse' 1 require 'recurse'
3 2
4 class Notice 3 class Notice
@@ -10,23 +9,20 @@ class Notice @@ -10,23 +9,20 @@ class Notice
10 field :request, :type => Hash 9 field :request, :type => Hash
11 field :notifier, :type => Hash 10 field :notifier, :type => Hash
12 field :user_attributes, :type => Hash 11 field :user_attributes, :type => Hash
13 - field :current_user, :type => Hash 12 + field :framework
14 field :error_class 13 field :error_class
15 delegate :lines, :to => :backtrace, :prefix => true 14 delegate :lines, :to => :backtrace, :prefix => true
16 delegate :app, :problem, :to => :err 15 delegate :app, :problem, :to => :err
17 16
18 belongs_to :err 17 belongs_to :err
19 belongs_to :backtrace, :index => true 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 before_save :sanitize 26 before_save :sanitize
31 before_destroy :decrease_counter_cache, :remove_cached_attributes_from_problem 27 before_destroy :decrease_counter_cache, :remove_cached_attributes_from_problem
32 28
@@ -42,7 +38,11 @@ class Notice @@ -42,7 +38,11 @@ class Notice
42 end 38 end
43 39
44 def user_agent_string 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 end 46 end
47 47
48 def environment_name 48 def environment_name
@@ -98,20 +98,29 @@ class Notice @@ -98,20 +98,29 @@ class Notice
98 problem.notices_count 98 problem.notices_count
99 end 99 end
100 100
101 - def notifiable? 101 + def emailable?
102 app.email_at_notices.include?(similar_count) 102 app.email_at_notices.include?(similar_count)
103 end 103 end
104 104
105 - def should_notify?  
106 - app.notifiable? && notifiable? 105 + def should_email?
  106 + app.emailable? && emailable?
107 end 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 end 120 end
114 121
  122 + protected
  123 +
115 def decrease_counter_cache 124 def decrease_counter_cache
116 problem.inc(:notices_count, -1) if err 125 problem.inc(:notices_count, -1) if err
117 end 126 end
@@ -125,7 +134,7 @@ class Notice @@ -125,7 +134,7 @@ class Notice
125 end 134 end
126 135
127 def cache_attributes_on_problem 136 def cache_attributes_on_problem
128 - problem.cache_notice_attributes(self) 137 + ProblemUpdaterCache.new(problem, self).update
129 end 138 end
130 139
131 def sanitize 140 def sanitize
@@ -134,6 +143,7 @@ class Notice @@ -134,6 +143,7 @@ class Notice
134 end 143 end
135 end 144 end
136 145
  146 +
137 def sanitize_hash(h) 147 def sanitize_hash(h)
138 h.recurse do 148 h.recurse do
139 |h| h.inject({}) do |h,(k,v)| 149 |h| h.inject({}) do |h,(k,v)|
@@ -147,5 +157,25 @@ class Notice @@ -147,5 +157,25 @@ class Notice
147 end 157 end
148 end 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 end 180 end
151 181
app/models/notice_observer.rb
@@ -1,13 +0,0 @@ @@ -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,14 +5,31 @@ class NotificationService
5 default_url_options[:host] = ActionMailer::Base.default_url_options[:host] 5 default_url_options[:host] = ActionMailer::Base.default_url_options[:host]
6 6
7 field :room_id, :type => String 7 field :room_id, :type => String
  8 + field :user_id, :type => String
  9 + field :service_url, :type => String
  10 + field :service, :type => String
8 field :api_token, :type => String 11 field :api_token, :type => String
9 field :subdomain, :type => String 12 field :subdomain, :type => String
10 field :sender_name, :type => String 13 field :sender_name, :type => String
11 - 14 + field :notify_at_notices, :type => Array, :default => Errbit::Config.notify_at_notices
12 embedded_in :app, :inverse_of => :notification_service 15 embedded_in :app, :inverse_of => :notification_service
13 16
14 validate :check_params 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 # Subclasses are responsible for overwriting this method. 33 # Subclasses are responsible for overwriting this method.
17 def check_params; true; end 34 def check_params; true; end
18 35
@@ -34,4 +51,8 @@ class NotificationService @@ -34,4 +51,8 @@ class NotificationService
34 def configured? 51 def configured?
35 api_token.present? 52 api_token.present?
36 end 53 end
  54 +
  55 + def problem_url(problem)
  56 + "http://#{Errbit::Config.host}/apps/#{problem.app.id}/problems/#{problem.id}"
  57 + end
37 end 58 end
app/models/notification_services/campfire_service.rb
1 if defined? Campy 1 if defined? Campy
2 class NotificationServices::CampfireService < NotificationService 2 class NotificationServices::CampfireService < NotificationService
3 Label = "campfire" 3 Label = "campfire"
4 - Fields = [ 4 + Fields += [
5 [:subdomain, { 5 [:subdomain, {
6 :label => "Subdomain", 6 :label => "Subdomain",
7 :placeholder => "subdomain from http://{{subdomain}}.campfirenow.com" 7 :placeholder => "subdomain from http://{{subdomain}}.campfirenow.com"
@@ -33,4 +33,4 @@ if defined? Campy @@ -33,4 +33,4 @@ if defined? Campy
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}" 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 end 34 end
35 end 35 end
36 -end  
37 \ No newline at end of file 36 \ No newline at end of file
  37 +end
app/models/notification_services/flowdock_service.rb 0 → 100644
@@ -0,0 +1,45 @@ @@ -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 class NotificationServices::GtalkService < NotificationService 1 class NotificationServices::GtalkService < NotificationService
2 Label = "gtalk" 2 Label = "gtalk"
3 - Fields = [ 3 + Fields += [
4 [:subdomain, { 4 [:subdomain, {
5 :placeholder => "username@example.com", 5 :placeholder => "username@example.com",
6 :label => "Username" 6 :label => "Username"
@@ -9,29 +9,64 @@ class NotificationServices::GtalkService &lt; NotificationService @@ -9,29 +9,64 @@ class NotificationServices::GtalkService &lt; NotificationService
9 :placeholder => "password", 9 :placeholder => "password",
10 :label => "Password" 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 [:room_id, { 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 def check_params 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 end 35 end
22 end 36 end
23 37
24 def url 38 def url
25 - "http://www.google.com/talk/" 39 + service_url || "http://www.google.com/talk/"
26 end 40 end
27 41
28 def create_notification(problem) 42 def create_notification(problem)
29 # build the xmpp client 43 # build the xmpp client
30 client = Jabber::Client.new(Jabber::JID.new(subdomain)) 44 client = Jabber::Client.new(Jabber::JID.new(subdomain))
31 - client.connect("talk.google.com") 45 + client.connect(service)
32 client.auth(api_token) 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 end 71 end
37 -end  
38 \ No newline at end of file 72 \ No newline at end of file
  73 +end
app/models/notification_services/hipchat_service.rb
1 if defined? HipChat 1 if defined? HipChat
2 class NotificationServices::HipchatService < NotificationService 2 class NotificationServices::HipchatService < NotificationService
3 Label = 'hipchat' 3 Label = 'hipchat'
4 - Fields = [ 4 + Fields += [
5 [:api_token, { 5 [:api_token, {
6 :placeholder => "API Token" 6 :placeholder => "API Token"
7 }], 7 }],
@@ -24,8 +24,9 @@ if defined? HipChat @@ -24,8 +24,9 @@ if defined? HipChat
24 def create_notification(problem) 24 def create_notification(problem)
25 url = app_problem_url problem.app, problem 25 url = app_problem_url problem.app, problem
26 message = <<-MSG.strip_heredoc 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 MSG 30 MSG
30 31
31 client = HipChat::Client.new(api_token) 32 client = HipChat::Client.new(api_token)
app/models/notification_services/hoiio_service.rb
1 class NotificationServices::HoiioService < NotificationService 1 class NotificationServices::HoiioService < NotificationService
2 Label = "hoiio" 2 Label = "hoiio"
3 - Fields = [ 3 + Fields += [
4 [:api_token, { 4 [:api_token, {
5 :placeholder => "App ID", 5 :placeholder => "App ID",
6 :label => "App ID" 6 :label => "App ID"
@@ -39,4 +39,4 @@ class NotificationServices::HoiioService &lt; NotificationService @@ -39,4 +39,4 @@ class NotificationServices::HoiioService &lt; NotificationService
39 end 39 end
40 40
41 end 41 end
42 -end  
43 \ No newline at end of file 42 \ No newline at end of file
  43 +end
app/models/notification_services/hubot_service.rb 0 → 100644
@@ -0,0 +1,32 @@ @@ -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 class NotificationServices::PushoverService < NotificationService 1 class NotificationServices::PushoverService < NotificationService
2 Label = "pushover" 2 Label = "pushover"
3 - Fields = [ 3 + Fields += [
4 [:api_token, { 4 [:api_token, {
5 :placeholder => "User Key", 5 :placeholder => "User Key",
6 :label => "User Key" 6 :label => "User Key"
@@ -29,4 +29,4 @@ class NotificationServices::PushoverService &lt; NotificationService @@ -29,4 +29,4 @@ class NotificationServices::PushoverService &lt; NotificationService
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") 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 end 31 end
32 -end  
33 \ No newline at end of file 32 \ No newline at end of file
  33 +end
app/models/notification_services/webhook_service.rb 0 → 100644
@@ -0,0 +1,19 @@ @@ -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,29 +26,39 @@ class Problem
26 field :hosts, :type => Hash, :default => {} 26 field :hosts, :type => Hash, :default => {}
27 field :comments_count, :type => Integer, :default => 0 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 belongs_to :app 38 belongs_to :app
39 has_many :errs, :inverse_of => :problem, :dependent => :destroy 39 has_many :errs, :inverse_of => :problem, :dependent => :destroy
40 has_many :comments, :inverse_of => :err, :dependent => :destroy 40 has_many :comments, :inverse_of => :err, :dependent => :destroy
41 41
  42 + validates_presence_of :environment
  43 +
42 before_create :cache_app_attributes 44 before_create :cache_app_attributes
43 45
44 scope :resolved, where(:resolved => true) 46 scope :resolved, where(:resolved => true)
45 scope :unresolved, where(:resolved => false) 47 scope :unresolved, where(:resolved => false)
46 scope :ordered, order_by(:last_notice_at.desc) 48 scope :ordered, order_by(:last_notice_at.desc)
47 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} 49 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}
48 - 50 +
49 validates_presence_of :last_notice_at, :first_notice_at 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 def self.in_env(env) 62 def self.in_env(env)
53 env.present? ? where(:environment => env) : scoped 63 env.present? ? where(:environment => env) : scoped
54 end 64 end
@@ -75,15 +85,7 @@ class Problem @@ -75,15 +85,7 @@ class Problem
75 85
76 86
77 def self.merge!(*problems) 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 end 89 end
88 90
89 def merged? 91 def merged?
@@ -91,11 +93,12 @@ class Problem @@ -91,11 +93,12 @@ class Problem
91 end 93 end
92 94
93 def unmerge! 95 def unmerge!
  96 + attrs = {:error_class => error_class, :environment => environment}
94 problem_errs = errs.to_a 97 problem_errs = errs.to_a
95 problem_errs.shift 98 problem_errs.shift
96 [self] + problem_errs.map(&:id).map do |err_id| 99 [self] + problem_errs.map(&:id).map do |err_id|
97 err = Err.find(err_id) 100 err = Err.find(err_id)
98 - app.problems.create.tap do |new_problem| 101 + app.problems.create(attrs).tap do |new_problem|
99 err.update_attribute(:problem_id, new_problem.id) 102 err.update_attribute(:problem_id, new_problem.id)
100 new_problem.reset_cached_attributes 103 new_problem.reset_cached_attributes
101 end 104 end
@@ -113,16 +116,14 @@ class Problem @@ -113,16 +116,14 @@ class Problem
113 else raise("\"#{sort}\" is not a recognized sort") 116 else raise("\"#{sort}\" is not a recognized sort")
114 end 117 end
115 end 118 end
116 - 119 +
117 def self.in_date_range(date_range) 120 def self.in_date_range(date_range)
118 where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}]) 121 where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}])
119 end 122 end
120 123
121 124
122 def reset_cached_attributes 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 end 127 end
127 128
128 def cache_app_attributes 129 def cache_app_attributes
@@ -131,32 +132,12 @@ class Problem @@ -131,32 +132,12 @@ class Problem
131 self.last_deploy_at = if (last_deploy = app.deploys.where(:environment => self.environment).last) 132 self.last_deploy_at = if (last_deploy = app.deploys.where(:environment => self.environment).last)
132 last_deploy.created_at.utc 133 last_deploy.created_at.utc
133 end 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 'last_deploy_at' => self.last_deploy_at.try(:utc)}}) 137 'last_deploy_at' => self.last_deploy_at.try(:utc)}})
137 end 138 end
138 end 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 def remove_cached_notice_attributes(notice) 141 def remove_cached_notice_attributes(notice)
161 update_attributes!( 142 update_attributes!(
162 :messages => attribute_count_descrease(:messages, notice.message), 143 :messages => attribute_count_descrease(:messages, notice.message),
@@ -171,16 +152,17 @@ class Problem @@ -171,16 +152,17 @@ class Problem
171 (app.issue_tracker_configured? && app.issue_tracker.label) || nil 152 (app.issue_tracker_configured? && app.issue_tracker.label) || nil
172 end 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 private 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 def attribute_count_descrease(name, value) 167 def attribute_count_descrease(name, value)
186 counter, index = send(name), attribute_index(value) 168 counter, index = send(name), attribute_index(value)
@@ -195,6 +177,5 @@ class Problem @@ -195,6 +177,5 @@ class Problem
195 def attribute_index(value) 177 def attribute_index(value)
196 Digest::MD5.hexdigest(value.to_s) 178 Digest::MD5.hexdigest(value.to_s)
197 end 179 end
198 -  
199 end 180 end
200 181
app/models/user.rb
@@ -13,14 +13,33 @@ class User @@ -13,14 +13,33 @@ class User
13 field :per_page, :type => Fixnum, :default => PER_PAGE 13 field :per_page, :type => Fixnum, :default => PER_PAGE
14 field :time_zone, :default => "UTC" 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 before_save :ensure_authentication_token 38 before_save :ensure_authentication_token
18 39
19 validates_presence_of :name 40 validates_presence_of :name
20 validates_uniqueness_of :github_login, :allow_nil => true 41 validates_uniqueness_of :github_login, :allow_nil => true
21 42
22 - attr_protected :admin  
23 -  
24 has_many :apps, :foreign_key => 'watchers.user_id' 43 has_many :apps, :foreign_key => 'watchers.user_id'
25 44
26 if Errbit::Config.user_has_username 45 if Errbit::Config.user_has_username
@@ -52,10 +71,12 @@ class User @@ -52,10 +71,12 @@ class User
52 github_account? && Errbit::Config.github_access_scope.include?('repo') 71 github_account? && Errbit::Config.github_access_scope.include?('repo')
53 end 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 end 77 end
  78 + self[:github_login] = login
  79 + end
  80 +
60 end 81 end
61 82
app/views/apps/_fields.html.haml
1 -= errors_for @app 1 += errors_for app
2 2
3 %div.required 3 %div.required
4 = f.label :name 4 = f.label :name
@@ -13,6 +13,10 @@ @@ -13,6 +13,10 @@
13 %div 13 %div
14 = f.label :bitbucket_repo 14 = f.label :bitbucket_repo
15 = f.text_field :bitbucket_repo, :placeholder => "errbit/errbit from https://bitbucket.org/errbit/errbit" 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 %fieldset 21 %fieldset
18 %legend Notifications 22 %legend Notifications
app/views/apps/_service_notification_fields.html.haml
@@ -17,7 +17,8 @@ @@ -17,7 +17,8 @@
17 - notification_service::Fields.each do |field, field_info| 17 - notification_service::Fields.each do |field, field_info|
18 = w.label field, field_info[:label] || field.to_s.titleize 18 = w.label field, field_info[:label] || field.to_s.titleize
19 - field_type = field == :password ? :password_field : :text_field 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 .image_preloader 23 .image_preloader
23 - (NotificationService.subclasses.map{|t| t.label } << 'none').each do |notification_service| 24 - (NotificationService.subclasses.map{|t| t.label } << 'none').each do |notification_service|
app/views/apps/edit.html.haml
1 - content_for :title, 'Edit App' 1 - content_for :title, 'Edit App'
2 - content_for :action_bar do 2 - content_for :action_bar do
3 = link_to_copy_attributes_from_other_app 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 = render 'fields', :f => f 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 - content_for :action_bar do 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 %table.apps 5 %table.apps
6 %thead 6 %thead
7 %tr 7 %tr
8 - %th Name 8 + %th= t('.name')
9 - if any_github_repos? || any_bitbucket_repos? 9 - if any_github_repos? || any_bitbucket_repos?
10 - %th Repository 10 + %th= t('.repository')
11 - if any_notification_services? 11 - if any_notification_services?
12 - %th Notification Service 12 + %th= t('.notify')
13 - if any_issue_trackers? 13 - if any_issue_trackers?
14 - %th Tracker 14 + %th= t('.tracker')
15 - if any_deploys? 15 - if any_deploys?
16 - %th Last Deploy  
17 - %th Errors 16 + %th= t('.last_deploy')
  17 + %th=t('.errors')
18 %tbody 18 %tbody
19 - - @apps.each do |app| 19 + - apps.each do |app|
20 %tr 20 %tr
21 %td.name= link_to app.name, app_path(app) 21 %td.name= link_to app.name, app_path(app)
22 - if any_github_repos? or any_bitbucket_repos? 22 - if any_github_repos? or any_bitbucket_repos?
@@ -50,10 +50,10 @@ @@ -50,10 +50,10 @@
50 - if app.problem_count > 0 50 - if app.problem_count > 0
51 - unresolved = app.unresolved_count 51 - unresolved = app.unresolved_count
52 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil) 52 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
53 - - if @apps.none? 53 + - if apps.none?
54 %tr 54 %tr
55 %td{:colspan => 3} 55 %td{:colspan => 3}
56 %em 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,7 +3,7 @@
3 = link_to_copy_attributes_from_other_app 3 = link_to_copy_attributes_from_other_app
4 = link_to('cancel', apps_path, :class => 'button') 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 = render 'fields', :f => f 8 = render 'fields', :f => f
9 9
app/views/apps/show.atom.builder
1 atom_feed do |feed| 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 render "problems/list", :feed => feed 3 render "problems/list", :feed => feed
4 end 4 end
app/views/apps/show.html.haml
1 -- content_for :title, @app.name 1 +- content_for :title, app.name
2 - content_for :head do 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 - content_for :meta do 4 - content_for :meta do
5 %strong Errors Caught: 5 %strong Errors Caught:
6 - = @app.problems.count 6 + = app.problems.count
7 %strong Deploy Count: 7 %strong Deploy Count:
8 - = @app.deploys.count 8 + = app.deploys.count
9 %strong API Key: 9 %strong API Key:
10 - = @app.api_key 10 + = app.api_key
11 - content_for :action_bar do 11 - content_for :action_bar do
12 - if current_user.admin? 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 - else 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 = link_to 'unwatch', app_watcher_path({:app_id => @app, :id => current_user.id}), :method => :delete, :class => 'button', :confirm => 'Are you sure?' 18 = link_to 'unwatch', app_watcher_path({:app_id => @app, :id => current_user.id}), :method => :delete, :class => 'button', :confirm => 'Are you sure?'
  19 +
20 %h3#watchers_toggle 20 %h3#watchers_toggle
21 Watchers 21 Watchers
22 %span.click_span (show/hide) 22 %span.click_span (show/hide)
23 #watchers_div 23 #watchers_div
24 - - if @app.notify_all_users 24 + - if app.notify_all_users
25 %table.watchers 25 %table.watchers
26 %thead 26 %thead
27 %tr 27 %tr
@@ -32,15 +32,15 @@ @@ -32,15 +32,15 @@
32 %tr 32 %tr
33 %th User or Email 33 %th User or Email
34 %tbody 34 %tbody
35 - - @app.watchers.each do |watcher| 35 + - app.watchers.each do |watcher|
36 %tr 36 %tr
37 %td= watcher.label 37 %td= watcher.label
38 - - if @app.watchers.none? 38 + - if app.watchers.none?
39 %tr 39 %tr
40 %td 40 %td
41 %em Sadly, no one is watching this app 41 %em Sadly, no one is watching this app
42 42
43 -- if @app.github_repo? 43 +- if app.github_repo?
44 %h3#repository_toggle 44 %h3#repository_toggle
45 Repository 45 Repository
46 %span.click_span (show/hide) 46 %span.click_span (show/hide)
@@ -51,13 +51,13 @@ @@ -51,13 +51,13 @@
51 %th GitHub Repo 51 %th GitHub Repo
52 %tbody 52 %tbody
53 %tr 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 %h3#deploys_toggle 56 %h3#deploys_toggle
57 Latest Deploys 57 Latest Deploys
58 %span.click_span (show/hide) 58 %span.click_span (show/hide)
59 #deploys_div 59 #deploys_div
60 - - if @deploys.any? 60 + - if deploys.any?
61 %table.deploys 61 %table.deploys
62 %thead 62 %thead
63 %tr 63 %tr
@@ -69,7 +69,7 @@ @@ -69,7 +69,7 @@
69 %th Revision 69 %th Revision
70 70
71 %tbody 71 %tbody
72 - - @deploys.each do |deploy| 72 + - deploys.each do |deploy|
73 %tr 73 %tr
74 %td.when #{deploy.created_at.to_s(:micro)} 74 %td.when #{deploy.created_at.to_s(:micro)}
75 %td.environment #{deploy.environment} 75 %td.environment #{deploy.environment}
@@ -77,14 +77,20 @@ @@ -77,14 +77,20 @@
77 %td.message #{deploy.message} 77 %td.message #{deploy.message}
78 %td.repository #{deploy.repository} 78 %td.repository #{deploy.repository}
79 %td.revision #{deploy.short_revision} 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 - else 81 - else
82 %h3 No deploys 82 %h3 No deploys
83 83
84 -- if @app.problems.any? 84 +- if app.problems.any?
85 %h3.clear Errors 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 - else 93 - else
88 %h3.clear No errs have been caught yet, make sure you setup your app 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,7 +9,7 @@
9 = form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| 9 = form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f|
10 .required 10 .required
11 = f.label auth_key 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 .required 14 .required
15 = link_to 'forget it?', new_password_path(resource_name), :class => 'float-right', :id => "forgot_password" 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 <% if notice = problem.notices.first %> 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 ## Params ## 2 ## Params ##
19 ``` 3 ```
20 <%= pretty_hash(notice.params) %> 4 <%= pretty_hash(notice.params) %>
app/views/issue_trackers/gitlab_summary.txt.erb 0 → 100644
@@ -0,0 +1,17 @@ @@ -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 \ No newline at end of file 18 \ No newline at end of file
app/views/issue_trackers/jira_body.txt.erb 0 → 100644
@@ -0,0 +1,17 @@ @@ -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 \ No newline at end of file 18 \ No newline at end of file
app/views/issue_trackers/redmine_body.txt.erb 0 → 100644
@@ -0,0 +1,17 @@ @@ -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 \ No newline at end of file 18 \ No newline at end of file
app/views/kaminari/notices/_paginator.html.haml
@@ -6,9 +6,9 @@ @@ -6,9 +6,9 @@
6 -# remote: data-remote 6 -# remote: data-remote
7 -# paginator: the paginator that renders the pagination tags inside 7 -# paginator: the paginator that renders the pagination tags inside
8 = paginator.render do 8 = paginator.render do
9 - .notice-pagination< 9 + .notice-pagination
10 = next_page_tag 10 = next_page_tag
11 - |&nbsp; 11 + |
12 = prev_page_tag 12 = prev_page_tag
13 .notice-pagination-loader= image_tag 'loader.gif' 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,7 +2,8 @@
2 %html 2 %html
3 %head 3 %head
4 %title 4 %title
5 - Errbit &mdash; 5 + = t('.title')
  6 + &mdash;
6 = yield(:page_title).present? ? yield(:page_title) : yield(:title) 7 = yield(:page_title).present? ? yield(:page_title) : yield(:title)
7 %meta{ :content => "text/html; charset=utf-8", "http-equiv" => "content-type" }/ 8 %meta{ :content => "text/html; charset=utf-8", "http-equiv" => "content-type" }/
8 = favicon_link_tag 9 = favicon_link_tag
@@ -14,7 +15,7 @@ @@ -14,7 +15,7 @@
14 %body{:id => controller.controller_name, :class => controller.action_name} 15 %body{:id => controller.controller_name, :class => controller.action_name}
15 #header 16 #header
16 %div 17 %div
17 - = link_to 'Errbit', root_path, :id => 'site-name' 18 + = link_to t('.errbit'), root_path, :id => 'site-name'
18 = render 'shared/navigation' if current_user 19 = render 'shared/navigation' if current_user
19 = render 'shared/session' 20 = render 'shared/session'
20 #content-wrapper 21 #content-wrapper
@@ -30,5 +31,5 @@ @@ -30,5 +31,5 @@
30 - if content_for?(:comments) 31 - if content_for?(:comments)
31 #content-comments 32 #content-comments
32 = yield :comments 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 = yield :scripts 35 = yield :scripts
app/views/mailer/comment_notification.html.haml 0 → 100644
@@ -0,0 +1,50 @@ @@ -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 @@ @@ -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 %>