Commit 09f663fc665a493e68fa17a7c9dbd0361733aa4d
Exists in
master
and in
1 other branch
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
.gitignore
.rspec
.travis.yml
| 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] |
| @@ -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 |
| @@ -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 |
Gemfile
| 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 'pivotal-tracker' | @@ -35,68 +38,87 @@ gem 'pivotal-tracker' | ||
| 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' |
Gemfile.lock
| 1 | +GIT | ||
| 2 | + remote: https://github.com/NARKOZ/gitlab.git | ||
| 3 | + revision: 7a00d38c53335010d2fb8a233247fe2c97338903 | ||
| 4 | + specs: | ||
| 5 | + gitlab (2.2.0) | ||
| 6 | + httparty | ||
| 7 | + | ||
| 1 | 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 |
LICENSE
| 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. |
Procfile
README.md
| 1 | -# Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url] | 1 | +# Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url] [![Coveralls][coveralls-img-url]][coveralls-url] [![Dependency Status][gemnasium-img-url]][gemnasium-url] |
| 2 | 2 | ||
| 3 | [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 |
1.18 KB
1.18 KB
1.1 KB
4.96 KB
4.96 KB
4.78 KB
1.97 KB
1.97 KB
1.97 KB
1.91 KB
2.29 KB
2.29 KB
2.11 KB
3.16 KB
3.16 KB
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 |
| @@ -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 < ActionController::Base | @@ -13,12 +13,28 @@ class ApplicationController < 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 < InheritedResources::Base | @@ -68,21 +95,11 @@ class AppsController < 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 < InheritedResources::Base | @@ -105,5 +122,20 @@ class AppsController < 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 < ApplicationController | @@ -17,4 +26,20 @@ class NoticesController < 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 |
| @@ -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 < ApplicationController | @@ -2,71 +2,76 @@ class UsersController < 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 |
| @@ -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 |
| @@ -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 |
| @@ -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/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 < ActionMailer::Base | @@ -20,5 +26,17 @@ class Mailer < 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/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 | - |
| @@ -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 |
| @@ -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 |
| @@ -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 |
| @@ -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 < NotificationService | @@ -9,29 +9,64 @@ class NotificationServices::GtalkService < 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 | + #{ERB::Util.html_escape problem.message.to_s.truncate(100)}<br> | ||
| 29 | + 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 < NotificationService | @@ -39,4 +39,4 @@ class NotificationServices::HoiioService < 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 |
| @@ -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 < NotificationService | @@ -29,4 +29,4 @@ class NotificationServices::PushoverService < 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 |
| @@ -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
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) %> |
| @@ -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 |
| @@ -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 |
| @@ -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 | - | | 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 — | 5 | + = t('.title') |
| 6 | + — | ||
| 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 |
| @@ -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 |
| @@ -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 %> |
app/views/mailer/err_notification.html.haml
| @@ -28,14 +28,31 @@ | @@ -28,14 +28,31 @@ | ||
| 28 | %p.monospace | 28 | %p.monospace |
| 29 | = @notice.where | 29 | = @notice.where |
| 30 | - @notice.in_app_backtrace_lines.each do |line| | 30 | - @notice.in_app_backtrace_lines.each do |line| |
| 31 | - %p.backtrace= line | ||
| 32 | - %br | 31 | + %p.backtrace |
| 32 | + = link_to_source_file(line) do | ||
| 33 | + = line.to_s | ||
| 34 | + %br | ||
| 33 | %p.heading URL: | 35 | %p.heading URL: |
| 34 | %p.monospace | 36 | %p.monospace |
| 35 | - if @notice.request['url'].present? | 37 | - if @notice.request['url'].present? |
| 36 | = link_to @notice.request['url'], @notice.request['url'] | 38 | = link_to @notice.request['url'], @notice.request['url'] |
| 37 | - %p.heading BACKTRACE: | ||
| 38 | - - @notice.backtrace_lines.each do |line| | ||
| 39 | - %p.backtrace= line | 39 | + %p.heading BROWSER: |
| 40 | + %p.monospace | ||
| 41 | + = user_agent_graph(@notice.problem) | ||
| 40 | %br | 42 | %br |
| 41 | - | 43 | + - if @notice.user_attributes.present? |
| 44 | + %p.heading USER: | ||
| 45 | + %table | ||
| 46 | + - @notice.user_attributes.each do |key, value| | ||
| 47 | + %tr | ||
| 48 | + %td(style="text-align: right; padding-right: 10px; color: #6a6a6a;")= key.to_s.titleize + ":" | ||
| 49 | + %td= auto_link(value.to_s) | ||
| 50 | + %br | ||
| 51 | + - if @notice.backtrace_lines.any? | ||
| 52 | + %br | ||
| 53 | + %p.heading FULL BACKTRACE: | ||
| 54 | + - @notice.backtrace_lines.each do |line| | ||
| 55 | + %p.backtrace | ||
| 56 | + = link_to_source_file(line) do | ||
| 57 | + = line.to_s | ||
| 58 | + %br |
app/views/mailer/err_notification.text.erb
| @@ -24,6 +24,20 @@ URL: | @@ -24,6 +24,20 @@ URL: | ||
| 24 | <%= @notice.request['url'] %> | 24 | <%= @notice.request['url'] %> |
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | +BROWSER: | ||
| 28 | + | ||
| 29 | +<%= @notice.user_agent_string %> | ||
| 30 | + | ||
| 31 | + | ||
| 32 | +<%- if @notice.user_attributes.present? %> | ||
| 33 | +USER: | ||
| 34 | + | ||
| 35 | +<%- @notice.user_attributes.each do |key, value| %> | ||
| 36 | +<%= key.to_s.titleize %>: <%= value.to_s %> | ||
| 37 | +<%- end %> | ||
| 38 | + | ||
| 39 | +<%- end %> | ||
| 40 | + | ||
| 27 | BACKTRACE: | 41 | BACKTRACE: |
| 28 | 42 | ||
| 29 | <% @notice.backtrace_lines.each do |line| %> | 43 | <% @notice.backtrace_lines.each do |line| %> |
app/views/notices/_backtrace_line.html.haml
| 1 | %tr{:class => defined?(row_class) && row_class} | 1 | %tr{:class => defined?(row_class) && row_class} |
| 2 | %td.line{:class => line.in_app? && 'in-app' } | 2 | %td.line{:class => line.in_app? && 'in-app' } |
| 3 | = link_to_source_file(line) do | 3 | = link_to_source_file(line) do |
| 4 | - %span.path>=raw line.decorated_path | 4 | + %span.path>= raw line.decorated_path |
| 5 | %span.file>= line.file_name | 5 | %span.file>= line.file_name |
| 6 | - if line.number.present? | 6 | - if line.number.present? |
| 7 | %span.number>= ":#{line.number}" | 7 | %span.number>= ":#{line.number}" |
| 8 | + - if line.column.present? | ||
| 9 | + %span.number>= ":#{line.column}" | ||
| 8 | → | 10 | → |
| 9 | %span.method= line.method | 11 | %span.method= line.method |
app/views/notices/_summary.html.haml
| @@ -28,6 +28,10 @@ | @@ -28,6 +28,10 @@ | ||
| 28 | %tr | 28 | %tr |
| 29 | %th App Server | 29 | %th App Server |
| 30 | %td= notice.server_environment && notice.server_environment["hostname"] | 30 | %td= notice.server_environment && notice.server_environment["hostname"] |
| 31 | + - if notice.framework | ||
| 32 | + %tr | ||
| 33 | + %th Framework | ||
| 34 | + %td= notice.framework | ||
| 31 | %tr | 35 | %tr |
| 32 | %th Rel. Directory | 36 | %th Rel. Directory |
| 33 | %td= notice.server_environment && notice.server_environment["project-root"] | 37 | %td= notice.server_environment && notice.server_environment["project-root"] |
app/views/notices/_user_attributes.html.haml
| @@ -2,8 +2,8 @@ | @@ -2,8 +2,8 @@ | ||
| 2 | %table.user_attributes | 2 | %table.user_attributes |
| 3 | %tr | 3 | %tr |
| 4 | %td | 4 | %td |
| 5 | - %strong Information about the user who experienced the error. | ||
| 6 | - - user.each do |user_key, user_value| | 5 | + %strong The user who experienced the error: |
| 6 | + - user.each do |key, value| | ||
| 7 | %tr | 7 | %tr |
| 8 | - %th= user_key | ||
| 9 | - %td= auto_link(user_value.to_s).html_safe | 8 | + %th= key |
| 9 | + %td= auto_link(auto_link(value.to_s, :urls, :target => "_blank"), :email_addresses).html_safe |
app/views/problems/_issue_tracker_links.html.haml
| 1 | -- if @app.issue_tracker_configured? || current_user.github_account? | ||
| 2 | - - if @problem.issue_link.present? | ||
| 3 | - %span= link_to 'go to issue', @problem.issue_link, :class => "#{@problem.issue_type}_goto goto-issue" | ||
| 4 | - = link_to 'unlink issue', unlink_issue_app_problem_path(@app, @problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue" | ||
| 5 | - - elsif @problem.issue_link == "pending" | ||
| 6 | - %span.disabled= link_to 'creating...', '#', :class => "#{@problem.issue_type}_inactive create-issue" | ||
| 7 | - = link_to 'retry', create_issue_app_problem_path(@app, @problem), :method => :post | 1 | +- if app.issue_tracker_configured? || current_user.github_account? |
| 2 | + - if problem.issue_link.present? | ||
| 3 | + %span= link_to 'go to issue', problem.issue_link, :class => "#{problem.issue_type}_goto goto-issue" | ||
| 4 | + = link_to 'unlink issue', unlink_issue_app_problem_path(app, problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue" | ||
| 5 | + - elsif problem.issue_link == "pending" | ||
| 6 | + %span.disabled= link_to 'creating...', '#', :class => "#{problem.issue_type}_inactive create-issue" | ||
| 7 | + = link_to 'retry', create_issue_app_problem_path(app, problem), :method => :post | ||
| 8 | - else | 8 | - else |
| 9 | - - if @app.github_repo? | 9 | + - if app.github_repo? |
| 10 | - if current_user.can_create_github_issues? | 10 | - if current_user.can_create_github_issues? |
| 11 | - %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem, :tracker => 'user_github'), :method => :post, :class => "github_create create-issue" | ||
| 12 | - - elsif @app.issue_tracker_configured? && @app.issue_tracker.is_a?(GithubIssuesTracker) | ||
| 13 | - %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem), :method => :post, :class => "github_create create-issue" | ||
| 14 | - - if @app.issue_tracker_configured? && !@app.issue_tracker.is_a?(GithubIssuesTracker) | ||
| 15 | - %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem), :method => :post, :class => "#{@app.issue_tracker.label}_create create-issue" | 11 | + %span= link_to 'create issue', create_issue_app_problem_path(app, problem, :tracker => 'user_github'), :method => :post, :class => "github_create create-issue" |
| 12 | + - elsif app.issue_tracker_configured? && app.issue_tracker.label.eql?('github') | ||
| 13 | + %span= link_to 'create issue', create_issue_app_problem_path(app, problem), :method => :post, :class => "github_create create-issue" | ||
| 14 | + - if app.issue_tracker_configured? && !app.issue_tracker.label.eql?('github') | ||
| 15 | + %span= link_to 'create issue', create_issue_app_problem_path(app, problem), :method => :post, :class => "#{app.issue_tracker.label}_create create-issue" |
app/views/problems/_list.atom.builder
| 1 | -feed.updated(@problems.first.try(:created_at) || Time.now) | 1 | +feed.updated(problems.first.try(:created_at) || Time.now) |
| 2 | 2 | ||
| 3 | -for problem in @problems | 3 | +for problem in problems |
| 4 | notice = problem.notices.first | 4 | notice = problem.notices.first |
| 5 | 5 | ||
| 6 | - feed.entry(problem, :url => app_problem_url(problem.app, problem)) do |entry| | 6 | + feed.entry(problem, :url => app_problem_url(problem.app.to_param, problem.to_param)) do |entry| |
| 7 | entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}" | 7 | entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}" |
| 8 | entry.author do |author| | 8 | entry.author do |author| |
| 9 | author.name "#{ problem.app.name } [#{ problem.environment }]" | 9 | author.name "#{ problem.app.name } [#{ problem.environment }]" |
app/views/problems/_table.html.haml
| @@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
| 16 | - problems.each do |problem| | 16 | - problems.each do |problem| |
| 17 | %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'} | 17 | %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'} |
| 18 | %td.select | 18 | %td.select |
| 19 | - = check_box_tag "problems[]", problem.id, @selected_problems.member?(problem.id.to_s) | 19 | + = check_box_tag "problems[]", problem.id, selected_problems.member?(problem.id.to_s) |
| 20 | %td.app | 20 | %td.app |
| 21 | = link_to problem.app.name, app_path(problem.app) | 21 | = link_to problem.app.name, app_path(problem.app) |
| 22 | - if current_page?(:controller => 'problems') | 22 | - if current_page?(:controller => 'problems') |
| @@ -48,9 +48,5 @@ | @@ -48,9 +48,5 @@ | ||
| 48 | = paginate problems | 48 | = paginate problems |
| 49 | .tab-bar | 49 | .tab-bar |
| 50 | %ul | 50 | %ul |
| 51 | - %li= submit_tag 'Merge', :id => 'merge_problems', :class => 'button', 'data-action' => merge_several_problems_path | ||
| 52 | - %li= submit_tag 'Unmerge', :id => 'unmerge_problems', :class => 'button', 'data-action' => unmerge_several_problems_path | ||
| 53 | - %li= submit_tag 'Resolve', :id => 'resolve_problems', :class => 'button', 'data-action' => resolve_several_problems_path | ||
| 54 | - %li= submit_tag 'Unresolve', :id => 'unresolve_problems', :class => 'button', 'data-action' => unresolve_several_problems_path | ||
| 55 | - %li= submit_tag 'Delete', :id => 'delete_problems', :class => 'button', 'data-action' => destroy_several_problems_path | ||
| 56 | - | 51 | + - %w(merge unmerge resolve unresolve delete).each do |action| |
| 52 | + %li= submit_tag action.capitalize, :id => "#{action}_problems", :class => 'button', :data => { :action => polymorphic_path([action == 'delete' ? 'destroy' : action, 'several_problems']), :confirm => problem_confirm } |
app/views/problems/all.html.haml
app/views/problems/index.html.haml
| 1 | -- content_for :title, 'Unresolved Errors' | 1 | +- content_for :title, @all_errs ? 'All Errors' : 'Unresolved Errors' |
| 2 | - content_for :head do | 2 | - content_for :head do |
| 3 | = auto_discovery_link_tag :atom, problems_path(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{request.host}" | 3 | = auto_discovery_link_tag :atom, problems_path(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{request.host}" |
| 4 | + | ||
| 4 | - content_for :action_bar do | 5 | - content_for :action_bar do |
| 5 | - = link_to 'show resolved', all_problems_path, :class => 'button' | ||
| 6 | -= render 'table', :problems => @problems | 6 | + - if @all_errs |
| 7 | + = link_to 'hide resolved', problems_path, :class => 'button' | ||
| 8 | + - else | ||
| 9 | + = link_to 'show resolved', problems_path(:all_errs => true), :class => 'button' | ||
| 10 | + | ||
| 11 | +%section | ||
| 12 | + = form_tag search_problems_path(:all_errs => @all_errs), :method => :get, :remote => true do | ||
| 13 | + = text_field_tag :search, params[:search], :placeholder => 'Search for issues' | ||
| 14 | +%br | ||
| 15 | +%section | ||
| 16 | + #problem_table.problem_table | ||
| 17 | + = render 'problems/table' |
app/views/problems/show.html.haml
| 1 | -- content_for :page_title, @problem.message | 1 | +- content_for :page_title, problem.message |
| 2 | - content_for :title_css_class, 'err_show' | 2 | - content_for :title_css_class, 'err_show' |
| 3 | -- content_for :title, @problem.error_class || truncate(@problem.message, :length => 32) | 3 | +- content_for :title, problem.error_class || truncate(problem.message, :length => 32) |
| 4 | - content_for :meta do | 4 | - content_for :meta do |
| 5 | %strong App: | 5 | %strong App: |
| 6 | - = link_to @app.name, @app | 6 | + = link_to app.name, app |
| 7 | %strong Where: | 7 | %strong Where: |
| 8 | - = @problem.where | 8 | + = problem.where |
| 9 | %br | 9 | %br |
| 10 | %strong Environment: | 10 | %strong Environment: |
| 11 | - = @problem.environment | 11 | + = problem.environment |
| 12 | %strong Last Notice: | 12 | %strong Last Notice: |
| 13 | - = @problem.last_notice_at.to_s(:precise) | 13 | + = problem.last_notice_at.to_s(:precise) |
| 14 | - content_for :action_bar do | 14 | - content_for :action_bar do |
| 15 | - - if @problem.unresolved? | ||
| 16 | - %span= link_to 'resolve', [:resolve, @app, @problem], :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve' | 15 | + - if problem.unresolved? |
| 16 | + %span= link_to 'resolve', [:resolve, app, problem], :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve' | ||
| 17 | - if current_user.authentication_token | 17 | - if current_user.authentication_token |
| 18 | - %span= link_to 'iCal', polymorphic_path([@app, @problem], :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link" | ||
| 19 | - %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_problems_path(@app)), :class => 'up' | 18 | + %span= link_to 'iCal', polymorphic_path([app, problem], :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link" |
| 19 | + %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_problems_path(app)), :class => 'up' | ||
| 20 | %br | 20 | %br |
| 21 | = render "issue_tracker_links" | 21 | = render "issue_tracker_links" |
| 22 | 22 | ||
| 23 | -- if @problem.comments_allowed? || @problem.comments.any? | 23 | +- if problem.comments_allowed? || problem.comments.any? |
| 24 | - content_for :comments do | 24 | - content_for :comments do |
| 25 | %h3 Comments | 25 | %h3 Comments |
| 26 | - - @problem.comments.each do |comment| | 26 | + - problem.comments.each do |comment| |
| 27 | .window | 27 | .window |
| 28 | %table.comment | 28 | %table.comment |
| 29 | %tr | 29 | %tr |
| @@ -37,11 +37,11 @@ | @@ -37,11 +37,11 @@ | ||
| 37 | - else | 37 | - else |
| 38 | %span.comment-info | 38 | %span.comment-info |
| 39 | = time_ago_in_words(comment.created_at, true) << " ago by [Unknown User]" | 39 | = time_ago_in_words(comment.created_at, true) << " ago by [Unknown User]" |
| 40 | - %span.delete= link_to '✘'.html_safe, [@app, @problem, comment], :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment" | 40 | + %span.delete= link_to '✘'.html_safe, [app, problem, comment], :method => :delete, :data => { :confirm => "Are you sure you don't need this comment?" }, :class => "destroy-comment" |
| 41 | %tr | 41 | %tr |
| 42 | %td= simple_format comment.body | 42 | %td= simple_format comment.body |
| 43 | - - if @problem.comments_allowed? | ||
| 44 | - = form_for [@app, @problem, @comment] do |comment_form| | 43 | + - if problem.comments_allowed? |
| 44 | + = form_for [app, problem, @comment] do |comment_form| | ||
| 45 | %p Add a comment | 45 | %p Add a comment |
| 46 | = comment_form.text_area :body | 46 | = comment_form.text_area :body |
| 47 | = comment_form.submit "Save Comment" | 47 | = comment_form.submit "Save Comment" |
| @@ -55,7 +55,7 @@ | @@ -55,7 +55,7 @@ | ||
| 55 | %li= link_to 'Summary', '#summary', :rel => 'summary', :class => 'button' | 55 | %li= link_to 'Summary', '#summary', :rel => 'summary', :class => 'button' |
| 56 | %li= link_to 'Backtrace', '#backtrace', :rel => 'backtrace', :class => 'button' | 56 | %li= link_to 'Backtrace', '#backtrace', :rel => 'backtrace', :class => 'button' |
| 57 | - if @notice && @notice.user_attributes.present? | 57 | - if @notice && @notice.user_attributes.present? |
| 58 | - %li= link_to 'User Details', '#user_attributes', :rel => 'user_attributes', :class => 'button' | 58 | + %li= link_to 'User', '#user_attributes', :rel => 'user_attributes', :class => 'button' |
| 59 | %li= link_to 'Environment', '#environment', :rel => 'environment', :class => 'button' | 59 | %li= link_to 'Environment', '#environment', :rel => 'environment', :class => 'button' |
| 60 | %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button' | 60 | %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button' |
| 61 | %li= link_to 'Session', '#session', :rel => 'session', :class => 'button' | 61 | %li= link_to 'Session', '#session', :rel => 'session', :class => 'button' |
| @@ -63,7 +63,7 @@ | @@ -63,7 +63,7 @@ | ||
| 63 | - if @notice | 63 | - if @notice |
| 64 | #summary | 64 | #summary |
| 65 | %h3 Summary | 65 | %h3 Summary |
| 66 | - = render 'notices/summary', :notice => @notice, :problem => @problem | 66 | + = render 'notices/summary', :notice => @notice |
| 67 | 67 | ||
| 68 | #backtrace | 68 | #backtrace |
| 69 | %h3 Backtrace | 69 | %h3 Backtrace |
| @@ -71,7 +71,7 @@ | @@ -71,7 +71,7 @@ | ||
| 71 | 71 | ||
| 72 | - if @notice.user_attributes.present? | 72 | - if @notice.user_attributes.present? |
| 73 | #user_attributes | 73 | #user_attributes |
| 74 | - %h3 User Details | 74 | + %h3 User |
| 75 | = render 'notices/user_attributes', :user => @notice.user_attributes | 75 | = render 'notices/user_attributes', :user => @notice.user_attributes |
| 76 | 76 | ||
| 77 | #environment | 77 | #environment |
app/views/problems/show.ics.haml
app/views/shared/_navigation.html.haml
| 1 | #nav-bar | 1 | #nav-bar |
| 2 | %ul | 2 | %ul |
| 3 | - //%li= link_to 'Dashboard', admin_dashboard_path, :class => active_if_here(:dashboards) | ||
| 4 | - %li.apps{:class => active_if_here(:apps)}= link_to 'Apps', apps_path | ||
| 5 | - %li.errs{:class => active_if_here(:problems)}= link_to 'Errors', problems_path | 3 | + %li.apps{:class => active_if_here(:apps)}= link_to t('.apps'), apps_path |
| 4 | + %li.errs{:class => active_if_here(:problems)}= link_to t('.errors'), problems_path | ||
| 6 | - if user_signed_in? && current_user.admin? | 5 | - if user_signed_in? && current_user.admin? |
| 7 | - %li.users{:class => active_if_here(:users)}= link_to 'Users', users_path | 6 | + %li.users{:class => active_if_here(:users)}= link_to t('.users'), users_path |
| 8 | %div.clear | 7 | %div.clear |
app/views/shared/_session.html.haml
| 1 | - if current_user | 1 | - if current_user |
| 2 | %ul#session-links | 2 | %ul#session-links |
| 3 | - %li= link_to 'Sign out', destroy_session_path(:user), :id => 'sign-out' | ||
| 4 | - %li= link_to 'Edit profile', edit_user_path(current_user), :id => 'edit-profile' | ||
| 5 | \ No newline at end of file | 3 | \ No newline at end of file |
| 4 | + %li= link_to t('.sign_out'), destroy_session_path(:user), :id => 'sign-out', :method => :delete | ||
| 5 | + %li= link_to t('.edit_profile'), edit_user_path(current_user), :id => 'edit-profile' |
app/views/users/_fields.html.haml
app/views/users/edit.html.haml
| 1 | -- content_for :title, "Edit #{@user.name}" | 1 | +- content_for :title, "Edit #{user.name}" |
| 2 | - content_for :action_bar do | 2 | - content_for :action_bar do |
| 3 | - = render 'shared/link_github_account', :user => @user | ||
| 4 | - = link_to('cancel', user_path(@user), :class => 'button') | 3 | + = render 'shared/link_github_account', :user => user |
| 4 | + = link_to('cancel', user_path(user), :class => 'button') | ||
| 5 | 5 | ||
| 6 | -= form_for @user, :html => {:autocomplete => "off"} do |f| | ||
| 7 | - = @user.errors.full_messages.to_sentence | 6 | += form_for user, :html => {:autocomplete => "off"} do |f| |
| 7 | + = user.errors.full_messages.to_sentence | ||
| 8 | = render 'fields', :f => f | 8 | = render 'fields', :f => f |
| 9 | 9 | ||
| 10 | %div.buttons= f.submit 'Update User' | 10 | %div.buttons= f.submit 'Update User' |
app/views/users/index.html.haml
| @@ -13,8 +13,8 @@ | @@ -13,8 +13,8 @@ | ||
| 13 | %th.main Email | 13 | %th.main Email |
| 14 | %th Admin? | 14 | %th Admin? |
| 15 | %tbody | 15 | %tbody |
| 16 | - - @users.each do |user| | ||
| 17 | - %tr | 16 | + - users.each do |user| |
| 17 | + %tr.user_list | ||
| 18 | - if Errbit::Config.use_gravatar | 18 | - if Errbit::Config.use_gravatar |
| 19 | %td= gravatar_tag user.email, :s => 24 | 19 | %td= gravatar_tag user.email, :s => 24 |
| 20 | %td.nowrap= link_to user.name, user_path(user) | 20 | %td.nowrap= link_to user.name, user_path(user) |
| @@ -22,5 +22,5 @@ | @@ -22,5 +22,5 @@ | ||
| 22 | %td= user.username | 22 | %td= user.username |
| 23 | %td= user.email | 23 | %td= user.email |
| 24 | %td= user.admin? ? 'Y' : 'N' | 24 | %td= user.admin? ? 'Y' : 'N' |
| 25 | -= paginate @users | 25 | += paginate users |
| 26 | 26 |
app/views/users/new.html.haml
| 1 | - content_for :title, 'New User' | 1 | - content_for :title, 'New User' |
| 2 | - content_for :action_bar, link_to('cancel', users_path, :class => 'button') | 2 | - content_for :action_bar, link_to('cancel', users_path, :class => 'button') |
| 3 | 3 | ||
| 4 | -= form_for @user do |f| | ||
| 5 | - | 4 | += form_for user do |f| |
| 5 | + | ||
| 6 | = render 'fields', :f => f | 6 | = render 'fields', :f => f |
| 7 | - | ||
| 8 | - %div.buttons= f.submit 'Add User' | ||
| 9 | \ No newline at end of file | 7 | \ No newline at end of file |
| 8 | + | ||
| 9 | + %div.buttons= f.submit 'Add User' |
app/views/users/show.html.haml
| 1 | -- content_for :title, @user.name | ||
| 2 | -- if Errbit::Config.use_gravatar | 1 | +- content_for :title, user.name |
| 2 | + | ||
| 3 | +- if Errbit::Config.use_gravatar && gravatar = gravatar_url(user.email, :s => 86) | ||
| 3 | - content_for :title_style do | 4 | - content_for :title_style do |
| 4 | - background: url('#{gravatar_url @user.email, :s => 86}') no-repeat; | 5 | + background: url('#{gravatar}') no-repeat; |
| 5 | padding-left: 106px; | 6 | padding-left: 106px; |
| 6 | 7 | ||
| 7 | - content_for :action_bar do | 8 | - content_for :action_bar do |
| 8 | - = render 'shared/link_github_account', :user => @user | 9 | + = render 'shared/link_github_account' |
| 9 | %span= link_to('Add a New User', new_user_path, :class => 'add') | 10 | %span= link_to('Add a New User', new_user_path, :class => 'add') |
| 10 | - = link_to 'edit', edit_user_path(@user), :class => 'button' | ||
| 11 | - = link_to 'destroy', user_path(@user), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button' | 11 | + = link_to 'edit', edit_user_path(user), :class => 'button' |
| 12 | + = link_to 'destroy', user_path(user), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button' | ||
| 12 | 13 | ||
| 13 | %table.single_user | 14 | %table.single_user |
| 14 | %tr | 15 | %tr |
| 15 | %th Email | 16 | %th Email |
| 16 | - %td.main= @user.email | 17 | + %td.main= user.email |
| 17 | - if Errbit::Config.user_has_username | 18 | - if Errbit::Config.user_has_username |
| 18 | %tr | 19 | %tr |
| 19 | %th Username | 20 | %th Username |
| 20 | - %td.main= @user.username | ||
| 21 | - - if Errbit::Config.github_authentication && @user.github_login.present? | 21 | + %td.main= user.username |
| 22 | + - if Errbit::Config.github_authentication && user.github_login.present? | ||
| 22 | %tr | 23 | %tr |
| 23 | %th GitHub Login | 24 | %th GitHub Login |
| 24 | - %td.main= link_to @user.github_login, "https://github.com/#{@user.github_login}" | 25 | + %td.main= link_to user.github_login, "https://github.com/#{user.github_login}" |
| 25 | %tr | 26 | %tr |
| 26 | %th Admin? | 27 | %th Admin? |
| 27 | - %td= @user.admin? ? 'Y' : 'N' | 28 | + %td= user.admin? ? 'Y' : 'N' |
| 28 | %tr | 29 | %tr |
| 29 | %th Created | 30 | %th Created |
| 30 | - %td= @user.created_at.to_s(:micro) | 31 | + %td= user.created_at.to_s(:micro) |
| 31 | 32 |
config.ru
| 1 | # This file is used by Rack-based servers to start the application. | 1 | # This file is used by Rack-based servers to start the application. |
| 2 | 2 | ||
| 3 | require ::File.expand_path('../config/environment', __FILE__) | 3 | require ::File.expand_path('../config/environment', __FILE__) |
| 4 | +use Rack::Deflater | ||
| 4 | run Errbit::Application | 5 | run Errbit::Application |
config/application.rb
| @@ -48,11 +48,14 @@ module Errbit | @@ -48,11 +48,14 @@ module Errbit | ||
| 48 | g.fixture_replacement :fabrication | 48 | g.fixture_replacement :fabrication |
| 49 | end | 49 | end |
| 50 | 50 | ||
| 51 | + # Enable the mongoid identity map for performance | ||
| 52 | + Mongoid.identity_map_enabled = true | ||
| 53 | + | ||
| 51 | # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. | 54 | # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. |
| 52 | config.mongoid.preload_models = true | 55 | config.mongoid.preload_models = true |
| 53 | 56 | ||
| 54 | # Set up observers | 57 | # Set up observers |
| 55 | - config.mongoid.observers = :deploy_observer, :notice_observer | 58 | + config.mongoid.observers = :deploy_observer, :comment_observer |
| 56 | 59 | ||
| 57 | # Configure the default encoding used in templates for Ruby 1.9. | 60 | # Configure the default encoding used in templates for Ruby 1.9. |
| 58 | config.encoding = "utf-8" | 61 | config.encoding = "utf-8" |
config/config.example.yml
| @@ -27,6 +27,15 @@ per_app_email_at_notices: false | @@ -27,6 +27,15 @@ per_app_email_at_notices: false | ||
| 27 | # an email notification. | 27 | # an email notification. |
| 28 | email_at_notices: [1, 10, 100] | 28 | email_at_notices: [1, 10, 100] |
| 29 | 29 | ||
| 30 | +# If you turn on this option, notify_at_notices can be | ||
| 31 | +# configured on a per app basis, at the App edit page | ||
| 32 | +per_app_notify_at_notices: false | ||
| 33 | + | ||
| 34 | +# Configure when emails are sent for an error. | ||
| 35 | +# [1,3,7] = 1st, 3rd, and 7th occurence triggers | ||
| 36 | +# [0] for all notices, provided notification service is configured | ||
| 37 | +notify_at_notices: [0] | ||
| 38 | + | ||
| 30 | # Configure whether or not the user should be prompted before resolving an error. | 39 | # Configure whether or not the user should be prompted before resolving an error. |
| 31 | confirm_resolve_err: true | 40 | confirm_resolve_err: true |
| 32 | 41 | ||
| @@ -39,6 +48,12 @@ user_has_username: false | @@ -39,6 +48,12 @@ user_has_username: false | ||
| 39 | # but you want to leave a short comment. | 48 | # but you want to leave a short comment. |
| 40 | allow_comments_with_issue_tracker: true | 49 | allow_comments_with_issue_tracker: true |
| 41 | 50 | ||
| 51 | +# Display internal errors in production | ||
| 52 | +# Since this is an internal application, you might like to see what caused Errbit to crash. | ||
| 53 | +# Pull requests are always welcome! | ||
| 54 | +# However, you might be more comfortable setting this to false if your server can be accessed by anyone. | ||
| 55 | +display_internal_errors: true | ||
| 56 | + | ||
| 42 | # Enable Gravatar. | 57 | # Enable Gravatar. |
| 43 | use_gravatar: true | 58 | use_gravatar: true |
| 44 | # Default Gravatar image, can be: mm, identicon, monsterid, wavatar, retro. | 59 | # Default Gravatar image, can be: mm, identicon, monsterid, wavatar, retro. |
| @@ -51,6 +66,7 @@ deployment: | @@ -51,6 +66,7 @@ deployment: | ||
| 51 | app: errbit.example.com | 66 | app: errbit.example.com |
| 52 | db: errbit.example.com | 67 | db: errbit.example.com |
| 53 | repository: http://github.com/errbit/errbit.git | 68 | repository: http://github.com/errbit/errbit.git |
| 69 | + branch: master | ||
| 54 | user: deploy | 70 | user: deploy |
| 55 | deploy_to: /var/www/apps/errbit | 71 | deploy_to: /var/www/apps/errbit |
| 56 | # setup path to unicorn pids folder (or deploy_to/shared/pids will be used) | 72 | # setup path to unicorn pids folder (or deploy_to/shared/pids will be used) |
| @@ -86,3 +102,8 @@ github_access_scope: ['repo'] | @@ -86,3 +102,8 @@ github_access_scope: ['repo'] | ||
| 86 | # :user_name: USERNAME | 102 | # :user_name: USERNAME |
| 87 | # :password: PASSWORD | 103 | # :password: PASSWORD |
| 88 | 104 | ||
| 105 | + | ||
| 106 | +# If you want send your email by your sendmail | ||
| 107 | +# sendmail_settings: | ||
| 108 | +# :location: '/usr/sbin/sendmail' | ||
| 109 | +# :arguments: '-i -t' |
config/deploy.example.rb
| @@ -35,10 +35,9 @@ set :copy_compression, :bz2 | @@ -35,10 +35,9 @@ set :copy_compression, :bz2 | ||
| 35 | 35 | ||
| 36 | set :scm, :git | 36 | set :scm, :git |
| 37 | set :scm_verbose, true | 37 | set :scm_verbose, true |
| 38 | -set(:current_branch) { `git branch`.match(/\* (\S+)\s/m)[1] || raise("Couldn't determine current branch") } | ||
| 39 | -set :branch, defer { current_branch } | 38 | +set :branch, config['branch'] || 'master' |
| 40 | 39 | ||
| 41 | -after 'deploy:update_code', 'errbit:symlink_configs' | 40 | +before 'deploy:assets:symlink', 'errbit:symlink_configs' |
| 42 | # if unicorn is started through something like runit (the tool which restarts the process when it's stopped) | 41 | # if unicorn is started through something like runit (the tool which restarts the process when it's stopped) |
| 43 | # after 'deploy:restart', 'unicorn:stop' | 42 | # after 'deploy:restart', 'unicorn:stop' |
| 44 | 43 | ||
| @@ -56,6 +55,12 @@ namespace :errbit do | @@ -56,6 +55,12 @@ namespace :errbit do | ||
| 56 | run "mkdir -p #{shared_configs}" | 55 | run "mkdir -p #{shared_configs}" |
| 57 | run "if [ ! -f #{shared_configs}/config.yml ]; then cp #{latest_release}/config/config.example.yml #{shared_configs}/config.yml; fi" | 56 | run "if [ ! -f #{shared_configs}/config.yml ]; then cp #{latest_release}/config/config.example.yml #{shared_configs}/config.yml; fi" |
| 58 | run "if [ ! -f #{shared_configs}/mongoid.yml ]; then cp #{latest_release}/config/mongoid.example.yml #{shared_configs}/mongoid.yml; fi" | 57 | run "if [ ! -f #{shared_configs}/mongoid.yml ]; then cp #{latest_release}/config/mongoid.example.yml #{shared_configs}/mongoid.yml; fi" |
| 58 | + | ||
| 59 | + # Generate unique secret token | ||
| 60 | + run %Q{if [ ! -f #{shared_configs}/secret_token.rb ]; then | ||
| 61 | + cd #{current_release}; | ||
| 62 | + echo "Errbit::Application.config.secret_token = '$(bundle exec rake secret)'" > #{shared_configs}/secret_token.rb; | ||
| 63 | + fi}.compact | ||
| 59 | end | 64 | end |
| 60 | 65 | ||
| 61 | task :symlink_configs do | 66 | task :symlink_configs do |
| @@ -64,6 +69,7 @@ namespace :errbit do | @@ -64,6 +69,7 @@ namespace :errbit do | ||
| 64 | release_configs = File.join(release_path,'config') | 69 | release_configs = File.join(release_path,'config') |
| 65 | run("ln -nfs #{shared_configs}/config.yml #{release_configs}/config.yml") | 70 | run("ln -nfs #{shared_configs}/config.yml #{release_configs}/config.yml") |
| 66 | run("ln -nfs #{shared_configs}/mongoid.yml #{release_configs}/mongoid.yml") | 71 | run("ln -nfs #{shared_configs}/mongoid.yml #{release_configs}/mongoid.yml") |
| 72 | + run("ln -nfs #{shared_configs}/secret_token.rb #{release_configs}/initializers/secret_token.rb") | ||
| 67 | end | 73 | end |
| 68 | end | 74 | end |
| 69 | 75 |
config/environment.rb
| 1 | -# Load the rails application | ||
| 2 | -require File.expand_path('../application', __FILE__) | ||
| 3 | if RUBY_VERSION.to_f >= 1.9 | 1 | if RUBY_VERSION.to_f >= 1.9 |
| 4 | require 'yaml' | 2 | require 'yaml' |
| 5 | YAML::ENGINE.yamler = 'syck' | 3 | YAML::ENGINE.yamler = 'syck' |
| 6 | end | 4 | end |
| 5 | + | ||
| 6 | +# Load the rails application | ||
| 7 | +require File.expand_path('../application', __FILE__) | ||
| 8 | + | ||
| 7 | # Initialize the rails application | 9 | # Initialize the rails application |
| 8 | Errbit::Application.initialize! | 10 | Errbit::Application.initialize! |
| 9 | - |
config/environments/development.rb
| @@ -14,7 +14,7 @@ Errbit::Application.configure do | @@ -14,7 +14,7 @@ Errbit::Application.configure do | ||
| 14 | config.action_controller.perform_caching = false | 14 | config.action_controller.perform_caching = false |
| 15 | 15 | ||
| 16 | # Don't care if the mailer can't send | 16 | # Don't care if the mailer can't send |
| 17 | - config.action_mailer.raise_delivery_errors = true | 17 | + config.action_mailer.raise_delivery_errors = false |
| 18 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } | 18 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } |
| 19 | 19 | ||
| 20 | # Print deprecation notices to the Rails logger | 20 | # Print deprecation notices to the Rails logger |
config/environments/production.rb
| @@ -5,8 +5,8 @@ Errbit::Application.configure do | @@ -5,8 +5,8 @@ Errbit::Application.configure do | ||
| 5 | # Code is not reloaded between requests | 5 | # Code is not reloaded between requests |
| 6 | config.cache_classes = true | 6 | config.cache_classes = true |
| 7 | 7 | ||
| 8 | - # Full error reports are enabled, since this is an internal application. | ||
| 9 | - config.consider_all_requests_local = true | 8 | + # Shows or hides all error details if something goes wrong inside Errbit |
| 9 | + config.consider_all_requests_local = false | ||
| 10 | # Caching is turned on | 10 | # Caching is turned on |
| 11 | config.action_controller.perform_caching = true | 11 | config.action_controller.perform_caching = true |
| 12 | 12 | ||
| @@ -59,5 +59,6 @@ Errbit::Application.configure do | @@ -59,5 +59,6 @@ Errbit::Application.configure do | ||
| 59 | 59 | ||
| 60 | # Send deprecation notices to registered listeners | 60 | # Send deprecation notices to registered listeners |
| 61 | config.active_support.deprecation = :notify | 61 | config.active_support.deprecation = :notify |
| 62 | + config.static_cache_control = "public, max-age=7200" | ||
| 62 | end | 63 | end |
| 63 | 64 |
config/initializers/_load_config.rb
| @@ -4,15 +4,17 @@ default_config_file = Rails.root.join("config", "config.example.yml") | @@ -4,15 +4,17 @@ default_config_file = Rails.root.join("config", "config.example.yml") | ||
| 4 | # Allow a Rails Engine to override config by defining it earlier | 4 | # Allow a Rails Engine to override config by defining it earlier |
| 5 | unless defined?(Errbit::Config) | 5 | unless defined?(Errbit::Config) |
| 6 | Errbit::Config = OpenStruct.new | 6 | Errbit::Config = OpenStruct.new |
| 7 | + use_env = ENV['HEROKU'] || ENV['USE_ENV'] | ||
| 7 | 8 | ||
| 8 | # If Errbit is running on Heroku, config can be set from environment variables. | 9 | # If Errbit is running on Heroku, config can be set from environment variables. |
| 9 | - if ENV['HEROKU'] | 10 | + if use_env |
| 10 | Errbit::Config.host = ENV['ERRBIT_HOST'] | 11 | Errbit::Config.host = ENV['ERRBIT_HOST'] |
| 11 | Errbit::Config.email_from = ENV['ERRBIT_EMAIL_FROM'] | 12 | Errbit::Config.email_from = ENV['ERRBIT_EMAIL_FROM'] |
| 12 | - Errbit::Config.email_at_notices = ENV['ERRBIT_EMAIL_AT_NOTICES'] | ||
| 13 | - Errbit::Config.confirm_resolve_err = ENV['ERRBIT_CONFIRM_RESOLVE_ERR'] | ||
| 14 | - Errbit::Config.user_has_username = ENV['ERRBIT_USER_HAS_USERNAME'] | ||
| 15 | - Errbit::Config.allow_comments_with_issue_tracker = ENV['ERRBIT_ALLOW_COMMENTS_WITH_ISSUE_TRACKER'] | 13 | + # Not really easy to use like an env because need an array and ENV return a string :( |
| 14 | + # Errbit::Config.email_at_notices = ENV['ERRBIT_EMAIL_AT_NOTICES'] | ||
| 15 | + Errbit::Config.confirm_resolve_err = ENV['ERRBIT_CONFIRM_RESOLVE_ERR'].to_i == 0 | ||
| 16 | + Errbit::Config.user_has_username = ENV['ERRBIT_USER_HAS_USERNAME'].to_i == 1 | ||
| 17 | + Errbit::Config.allow_comments_with_issue_tracker = ENV['ERRBIT_ALLOW_COMMENTS_WITH_ISSUE_TRACKER'].to_i == 0 | ||
| 16 | Errbit::Config.enforce_ssl = ENV['ERRBIT_ENFORCE_SSL'] | 18 | Errbit::Config.enforce_ssl = ENV['ERRBIT_ENFORCE_SSL'] |
| 17 | 19 | ||
| 18 | Errbit::Config.use_gravatar = ENV['ERRBIT_USE_GRAVATAR'] | 20 | Errbit::Config.use_gravatar = ENV['ERRBIT_USE_GRAVATAR'] |
| @@ -24,12 +26,12 @@ unless defined?(Errbit::Config) | @@ -24,12 +26,12 @@ unless defined?(Errbit::Config) | ||
| 24 | Errbit::Config.github_access_scope = ENV['GITHUB_ACCESS_SCOPE'].split(',').map(&:strip) if ENV['GITHUB_ACCESS_SCOPE'] | 26 | Errbit::Config.github_access_scope = ENV['GITHUB_ACCESS_SCOPE'].split(',').map(&:strip) if ENV['GITHUB_ACCESS_SCOPE'] |
| 25 | 27 | ||
| 26 | Errbit::Config.smtp_settings = { | 28 | Errbit::Config.smtp_settings = { |
| 27 | - :address => "smtp.sendgrid.net", | ||
| 28 | - :port => "25", | 29 | + :address => ENV['SMTP_SERVER'] || 'smtp.sendgrid.net', |
| 30 | + :port => ENV['SMTP_PORT'] || 25, | ||
| 29 | :authentication => :plain, | 31 | :authentication => :plain, |
| 30 | - :user_name => ENV['SENDGRID_USERNAME'], | ||
| 31 | - :password => ENV['SENDGRID_PASSWORD'], | ||
| 32 | - :domain => ENV['SENDGRID_DOMAIN'] | 32 | + :user_name => ENV['SMTP_USERNAME'] || ENV['SENDGRID_USERNAME'], |
| 33 | + :password => ENV['SMTP_PASSWORD'] || ENV['SENDGRID_PASSWORD'], | ||
| 34 | + :domain => ENV['SMTP_DOMAIN'] || ENV['SENDGRID_DOMAIN'] || ENV['ERRBIT_EMAIL_FROM'].split('@').last | ||
| 33 | } | 35 | } |
| 34 | end | 36 | end |
| 35 | 37 | ||
| @@ -44,7 +46,7 @@ unless defined?(Errbit::Config) | @@ -44,7 +46,7 @@ unless defined?(Errbit::Config) | ||
| 44 | Errbit::Config.send("#{k}=", v) | 46 | Errbit::Config.send("#{k}=", v) |
| 45 | end | 47 | end |
| 46 | # Show message if we are not running tests, not running on Heroku, and config.yml doesn't exist. | 48 | # Show message if we are not running tests, not running on Heroku, and config.yml doesn't exist. |
| 47 | - elsif not ENV['HEROKU'] | 49 | + elsif not use_env |
| 48 | puts "Please copy 'config/config.example.yml' to 'config/config.yml' and configure your settings. Using default settings." | 50 | puts "Please copy 'config/config.example.yml' to 'config/config.yml' and configure your settings. Using default settings." |
| 49 | end | 51 | end |
| 50 | 52 | ||
| @@ -69,8 +71,16 @@ if smtp = Errbit::Config.smtp_settings | @@ -69,8 +71,16 @@ if smtp = Errbit::Config.smtp_settings | ||
| 69 | ActionMailer::Base.smtp_settings = smtp | 71 | ActionMailer::Base.smtp_settings = smtp |
| 70 | end | 72 | end |
| 71 | 73 | ||
| 74 | +if sendmail = Errbit::Config.sendmail_settings | ||
| 75 | + ActionMailer::Base.delivery_method = :sendmail | ||
| 76 | + ActionMailer::Base.sendmail_settings = sendmail | ||
| 77 | +end | ||
| 78 | + | ||
| 72 | # Set config specific values | 79 | # Set config specific values |
| 73 | (ActionMailer::Base.default_url_options ||= {}).tap do |default| | 80 | (ActionMailer::Base.default_url_options ||= {}).tap do |default| |
| 74 | default.merge! :host => Errbit::Config.host if default[:host].blank? | 81 | default.merge! :host => Errbit::Config.host if default[:host].blank? |
| 75 | end | 82 | end |
| 76 | 83 | ||
| 84 | +if Rails.env.production? | ||
| 85 | + Rails.application.config.consider_all_requests_local = Errbit::Config.display_internal_errors | ||
| 86 | +end |
config/initializers/devise.rb
| 1 | -# Use this hook to configure devise mailer, warden hooks and so forth. The first | ||
| 2 | -# four configuration values can also be set straight in your models. | 1 | +# Use this hook to configure devise mailer, warden hooks and so forth. |
| 2 | +# Many of these configuration options can be set straight in your model. | ||
| 3 | Devise.setup do |config| | 3 | Devise.setup do |config| |
| 4 | # ==> Mailer Configuration | 4 | # ==> Mailer Configuration |
| 5 | - # Configure the e-mail address which will be shown in DeviseMailer. | 5 | + # Configure the e-mail address which will be shown in Devise::Mailer, |
| 6 | + # note that it will be overwritten if you use your own mailer class with default "from" parameter. | ||
| 6 | config.mailer_sender = Errbit::Config.email_from | 7 | config.mailer_sender = Errbit::Config.email_from |
| 7 | 8 | ||
| 8 | # Configure the class responsible to send e-mails. | 9 | # Configure the class responsible to send e-mails. |
| @@ -15,69 +16,131 @@ Devise.setup do |config| | @@ -15,69 +16,131 @@ Devise.setup do |config| | ||
| 15 | require 'devise/orm/mongoid' | 16 | require 'devise/orm/mongoid' |
| 16 | 17 | ||
| 17 | # ==> Configuration for any authentication mechanism | 18 | # ==> Configuration for any authentication mechanism |
| 18 | - # Configure which keys are used when authenticating an user. By default is | 19 | + # Configure which keys are used when authenticating a user. The default is |
| 19 | # just :email. You can configure it to use [:username, :subdomain], so for | 20 | # just :email. You can configure it to use [:username, :subdomain], so for |
| 20 | - # authenticating an user, both parameters are required. Remember that those | 21 | + # authenticating a user, both parameters are required. Remember that those |
| 21 | # parameters are used only when authenticating and not when retrieving from | 22 | # parameters are used only when authenticating and not when retrieving from |
| 22 | # session. If you need permissions, you should implement that in a before filter. | 23 | # session. If you need permissions, you should implement that in a before filter. |
| 24 | + # You can also supply a hash where the value is a boolean determining whether | ||
| 25 | + # or not authentication should be aborted when the value is not present. | ||
| 23 | config.authentication_keys = [ Errbit::Config.user_has_username ? :username : :email ] | 26 | config.authentication_keys = [ Errbit::Config.user_has_username ? :username : :email ] |
| 24 | 27 | ||
| 28 | + # Configure parameters from the request object used for authentication. Each entry | ||
| 29 | + # given should be a request method and it will automatically be passed to the | ||
| 30 | + # find_for_authentication method and considered in your model lookup. For instance, | ||
| 31 | + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. | ||
| 32 | + # The same considerations mentioned for authentication_keys also apply to request_keys. | ||
| 33 | + # config.request_keys = [] | ||
| 34 | + | ||
| 35 | + # Configure which authentication keys should be case-insensitive. | ||
| 36 | + # These keys will be downcased upon creating or modifying a user and when used | ||
| 37 | + # to authenticate or find a user. Default is :email. | ||
| 38 | + config.case_insensitive_keys = [ Errbit::Config.user_has_username ? :username : :email ] | ||
| 39 | + | ||
| 40 | + # Configure which authentication keys should have whitespace stripped. | ||
| 41 | + # These keys will have whitespace before and after removed upon creating or | ||
| 42 | + # modifying a user and when used to authenticate or find a user. Default is :email. | ||
| 43 | + config.strip_whitespace_keys = [ Errbit::Config.user_has_username ? :username : :email ] | ||
| 44 | + | ||
| 25 | # Tell if authentication through request.params is enabled. True by default. | 45 | # Tell if authentication through request.params is enabled. True by default. |
| 46 | + # It can be set to an array that will enable params authentication only for the | ||
| 47 | + # given strategies, for example, `config.params_authenticatable = [:database]` will | ||
| 48 | + # enable it only for database (email + password) authentication. | ||
| 26 | # config.params_authenticatable = true | 49 | # config.params_authenticatable = true |
| 27 | 50 | ||
| 28 | - # Tell if authentication through HTTP Basic Auth is enabled. True by default. | ||
| 29 | - # config.http_authenticatable = true | ||
| 30 | - | ||
| 31 | - # Set this to true to use Basic Auth for AJAX requests. True by default. | 51 | + # Tell if authentication through HTTP Auth is enabled. False by default. |
| 52 | + # It can be set to an array that will enable http authentication only for the | ||
| 53 | + # given strategies, for example, `config.http_authenticatable = [:token]` will | ||
| 54 | + # enable it only for token authentication. The supported strategies are: | ||
| 55 | + # :database = Support basic authentication with authentication key + password | ||
| 56 | + # :token = Support basic authentication with token authentication key | ||
| 57 | + # :token_options = Support token authentication with options as defined in | ||
| 58 | + # http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html | ||
| 59 | + # config.http_authenticatable = false | ||
| 60 | + | ||
| 61 | + # If http headers should be returned for AJAX requests. True by default. | ||
| 32 | # config.http_authenticatable_on_xhr = true | 62 | # config.http_authenticatable_on_xhr = true |
| 33 | 63 | ||
| 34 | - # The realm used in Http Basic Authentication | 64 | + # The realm used in Http Basic Authentication. "Application" by default. |
| 35 | # config.http_authentication_realm = "Application" | 65 | # config.http_authentication_realm = "Application" |
| 36 | 66 | ||
| 67 | + # It will change confirmation, password recovery and other workflows | ||
| 68 | + # to behave the same regardless if the e-mail provided was right or wrong. | ||
| 69 | + # Does not affect registerable. | ||
| 70 | + # config.paranoid = true | ||
| 71 | + | ||
| 72 | + # By default Devise will store the user in session. You can skip storage for | ||
| 73 | + # :http_auth and :token_auth by adding those symbols to the array below. | ||
| 74 | + # Notice that if you are skipping storage for all authentication paths, you | ||
| 75 | + # may want to disable generating routes to Devise's sessions controller by | ||
| 76 | + # passing :skip => :sessions to `devise_for` in your config/routes.rb | ||
| 77 | + config.skip_session_storage = [:http_auth] | ||
| 78 | + | ||
| 37 | # ==> Configuration for :database_authenticatable | 79 | # ==> Configuration for :database_authenticatable |
| 38 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If | 80 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If |
| 39 | # using other encryptors, it sets how many times you want the password re-encrypted. | 81 | # using other encryptors, it sets how many times you want the password re-encrypted. |
| 40 | - config.stretches = 10 | ||
| 41 | - | ||
| 42 | - # Define which will be the encryption algorithm. Devise also supports encryptors | ||
| 43 | - # from others authentication tools as :clearance_sha1, :authlogic_sha512 (then | ||
| 44 | - # you should set stretches above to 20 for default behavior) and :restful_authentication_sha1 | ||
| 45 | - # (then you should set stretches to 10, and copy REST_AUTH_SITE_KEY to pepper) | ||
| 46 | - config.encryptor = :bcrypt | 82 | + # |
| 83 | + # Limiting the stretches to just one in testing will increase the performance of | ||
| 84 | + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use | ||
| 85 | + # a value less than 10 in other environments. | ||
| 86 | + config.stretches = Rails.env.test? ? 1 : 10 | ||
| 47 | 87 | ||
| 48 | # Setup a pepper to generate the encrypted password. | 88 | # Setup a pepper to generate the encrypted password. |
| 49 | config.pepper = "425f10f555c1a4718aff3370ef9dd2d97a21622beb0400fde6b52177375ddcbe37a2dac6af9bca835c988e00c32887ee940ba111a78eab48234d8799936d36b9" | 89 | config.pepper = "425f10f555c1a4718aff3370ef9dd2d97a21622beb0400fde6b52177375ddcbe37a2dac6af9bca835c988e00c32887ee940ba111a78eab48234d8799936d36b9" |
| 50 | 90 | ||
| 51 | # ==> Configuration for :confirmable | 91 | # ==> Configuration for :confirmable |
| 52 | - # The time you want to give your user to confirm his account. During this time | ||
| 53 | - # he will be able to access your application without confirming. Default is nil. | ||
| 54 | - # When confirm_within is zero, the user won't be able to sign in without confirming. | ||
| 55 | - # You can use this to let your user access some features of your application | ||
| 56 | - # without confirming the account, but blocking it after a certain period | ||
| 57 | - # (ie 2 days). | ||
| 58 | - # config.confirm_within = 2.days | 92 | + # A period that the user is allowed to access the website even without |
| 93 | + # confirming his account. For instance, if set to 2.days, the user will be | ||
| 94 | + # able to access the website for two days without confirming his account, | ||
| 95 | + # access will be blocked just in the third day. Default is 0.days, meaning | ||
| 96 | + # the user cannot access the website without confirming his account. | ||
| 97 | + # config.allow_unconfirmed_access_for = 2.days | ||
| 98 | + | ||
| 99 | + # A period that the user is allowed to confirm their account before their | ||
| 100 | + # token becomes invalid. For example, if set to 3.days, the user can confirm | ||
| 101 | + # their account within 3 days after the mail was sent, but on the fourth day | ||
| 102 | + # their account can't be confirmed with the token any more. | ||
| 103 | + # Default is nil, meaning there is no restriction on how long a user can take | ||
| 104 | + # before confirming their account. | ||
| 105 | + # config.confirm_within = 3.days | ||
| 106 | + | ||
| 107 | + # If true, requires any email changes to be confirmed (exactly the same way as | ||
| 108 | + # initial account confirmation) to be applied. Requires additional unconfirmed_email | ||
| 109 | + # db field (see migrations). Until confirmed new email is stored in | ||
| 110 | + # unconfirmed email column, and copied to email column on successful confirmation. | ||
| 111 | + config.reconfirmable = true | ||
| 112 | + | ||
| 113 | + # Defines which key will be used when confirming an account | ||
| 114 | + # config.confirmation_keys = [ :email ] | ||
| 59 | 115 | ||
| 60 | # ==> Configuration for :rememberable | 116 | # ==> Configuration for :rememberable |
| 61 | # The time the user will be remembered without asking for credentials again. | 117 | # The time the user will be remembered without asking for credentials again. |
| 62 | config.remember_for = 2.weeks | 118 | config.remember_for = 2.weeks |
| 63 | 119 | ||
| 64 | - # If true, a valid remember token can be re-used between multiple browsers. | ||
| 65 | - # config.remember_across_browsers = true | ||
| 66 | - | ||
| 67 | # If true, extends the user's remember period when remembered via cookie. | 120 | # If true, extends the user's remember period when remembered via cookie. |
| 68 | # config.extend_remember_period = false | 121 | # config.extend_remember_period = false |
| 69 | 122 | ||
| 123 | + # Options to be passed to the created cookie. For instance, you can set | ||
| 124 | + # :secure => true in order to force SSL only cookies. | ||
| 125 | + # config.rememberable_options = {} | ||
| 126 | + | ||
| 70 | # ==> Configuration for :validatable | 127 | # ==> Configuration for :validatable |
| 71 | - # Range for password length | ||
| 72 | - config.password_length = 6..20 | 128 | + # Range for password length. Default is 8..128. |
| 129 | + config.password_length = 6..1024 | ||
| 73 | 130 | ||
| 74 | - # Regex to use to validate the email address | 131 | + # Email regex used to validate email formats. It simply asserts that |
| 132 | + # one (and only one) @ exists in the given string. This is mainly | ||
| 133 | + # to give user feedback and not to assert the e-mail validity. | ||
| 134 | + # config.email_regexp = /\A[^@]+@[^@]+\z/ | ||
| 75 | config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i | 135 | config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i |
| 76 | 136 | ||
| 77 | # ==> Configuration for :timeoutable | 137 | # ==> Configuration for :timeoutable |
| 78 | # The time you want to timeout the user session without activity. After this | 138 | # The time you want to timeout the user session without activity. After this |
| 79 | - # time the user will be asked for credentials again. | ||
| 80 | - # config.timeout_in = 10.minutes | 139 | + # time the user will be asked for credentials again. Default is 30 minutes. |
| 140 | + # config.timeout_in = 30.minutes | ||
| 141 | + | ||
| 142 | + # If true, expires auth token on session timeout. | ||
| 143 | + # config.expire_auth_token_on_timeout = false | ||
| 81 | 144 | ||
| 82 | # ==> Configuration for :lockable | 145 | # ==> Configuration for :lockable |
| 83 | # Defines which strategy will be used to lock an account. | 146 | # Defines which strategy will be used to lock an account. |
| @@ -85,6 +148,9 @@ Devise.setup do |config| | @@ -85,6 +148,9 @@ Devise.setup do |config| | ||
| 85 | # :none = No lock strategy. You should handle locking by yourself. | 148 | # :none = No lock strategy. You should handle locking by yourself. |
| 86 | # config.lock_strategy = :failed_attempts | 149 | # config.lock_strategy = :failed_attempts |
| 87 | 150 | ||
| 151 | + # Defines which key will be used when locking and unlocking an account | ||
| 152 | + # config.unlock_keys = [ :email ] | ||
| 153 | + | ||
| 88 | # Defines which strategy will be used to unlock an account. | 154 | # Defines which strategy will be used to unlock an account. |
| 89 | # :email = Sends an unlock link to the user email | 155 | # :email = Sends an unlock link to the user email |
| 90 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) | 156 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) |
| @@ -99,6 +165,26 @@ Devise.setup do |config| | @@ -99,6 +165,26 @@ Devise.setup do |config| | ||
| 99 | # Time interval to unlock the account if :time is enabled as unlock_strategy. | 165 | # Time interval to unlock the account if :time is enabled as unlock_strategy. |
| 100 | # config.unlock_in = 1.hour | 166 | # config.unlock_in = 1.hour |
| 101 | 167 | ||
| 168 | + # ==> Configuration for :recoverable | ||
| 169 | + # | ||
| 170 | + # Defines which key will be used when recovering the password for an account | ||
| 171 | + # config.reset_password_keys = [ :email ] | ||
| 172 | + | ||
| 173 | + # Time interval you can reset your password with a reset password key. | ||
| 174 | + # Don't put a too small interval or your users won't have the time to | ||
| 175 | + # change their passwords. | ||
| 176 | + config.reset_password_within = 6.hours | ||
| 177 | + | ||
| 178 | + # ==> Configuration for :encryptable | ||
| 179 | + # Allow you to use another encryption algorithm besides bcrypt (default). You can use | ||
| 180 | + # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, | ||
| 181 | + # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) | ||
| 182 | + # and :restful_authentication_sha1 (then you should set stretches to 10, and copy | ||
| 183 | + # REST_AUTH_SITE_KEY to pepper). | ||
| 184 | + # | ||
| 185 | + # Require the `devise-encryptable` gem when using anything other than bcrypt | ||
| 186 | + # config.encryptor = :sha512 | ||
| 187 | + | ||
| 102 | # ==> Configuration for :token_authenticatable | 188 | # ==> Configuration for :token_authenticatable |
| 103 | # Defines name of the authentication token params key | 189 | # Defines name of the authentication token params key |
| 104 | config.token_authentication_key = :auth_token | 190 | config.token_authentication_key = :auth_token |
| @@ -107,45 +193,63 @@ Devise.setup do |config| | @@ -107,45 +193,63 @@ Devise.setup do |config| | ||
| 107 | # Turn scoped views on. Before rendering "sessions/new", it will first check for | 193 | # Turn scoped views on. Before rendering "sessions/new", it will first check for |
| 108 | # "users/sessions/new". It's turned off by default because it's slower if you | 194 | # "users/sessions/new". It's turned off by default because it's slower if you |
| 109 | # are using only default views. | 195 | # are using only default views. |
| 110 | - # config.scoped_views = true | 196 | + # config.scoped_views = false |
| 111 | 197 | ||
| 112 | # Configure the default scope given to Warden. By default it's the first | 198 | # Configure the default scope given to Warden. By default it's the first |
| 113 | - # devise role declared in your routes. | 199 | + # devise role declared in your routes (usually :user). |
| 114 | # config.default_scope = :user | 200 | # config.default_scope = :user |
| 115 | 201 | ||
| 116 | - # Configure sign_out behavior. | ||
| 117 | - # By default sign_out is scoped (i.e. /users/sign_out affects only :user scope). | ||
| 118 | - # In case of sign_out_all_scopes set to true any logout action will sign out all active scopes. | ||
| 119 | - # config.sign_out_all_scopes = false | ||
| 120 | - | ||
| 121 | - if Errbit::Config.github_authentication || Rails.env.test? | ||
| 122 | - config.omniauth :github, | ||
| 123 | - Errbit::Config.github_client_id, | ||
| 124 | - Errbit::Config.github_secret, | ||
| 125 | - :scope => Errbit::Config.github_access_scope.join(","), | ||
| 126 | - :skip_info => true | ||
| 127 | - end | 202 | + # Set this configuration to false if you want /users/sign_out to sign out |
| 203 | + # only the current scope. By default, Devise signs out all scopes. | ||
| 204 | + # config.sign_out_all_scopes = true | ||
| 128 | 205 | ||
| 129 | # ==> Navigation configuration | 206 | # ==> Navigation configuration |
| 130 | # Lists the formats that should be treated as navigational. Formats like | 207 | # Lists the formats that should be treated as navigational. Formats like |
| 131 | # :html, should redirect to the sign in page when the user does not have | 208 | # :html, should redirect to the sign in page when the user does not have |
| 132 | # access, but formats like :xml or :json, should return 401. | 209 | # access, but formats like :xml or :json, should return 401. |
| 210 | + # | ||
| 133 | # If you have any extra navigational formats, like :iphone or :mobile, you | 211 | # If you have any extra navigational formats, like :iphone or :mobile, you |
| 134 | - # should add them to the navigational formats lists. Default is [:html] | ||
| 135 | - # config.navigational_formats = [:html, :iphone] | 212 | + # should add them to the navigational formats lists. |
| 213 | + # | ||
| 214 | + # The "*/*" below is required to match Internet Explorer requests. | ||
| 215 | + # config.navigational_formats = ["*/*", :html] | ||
| 216 | + | ||
| 217 | + # The default HTTP method used to sign out a resource. Default is :delete. | ||
| 218 | + config.sign_out_via = :delete | ||
| 219 | + | ||
| 220 | + # ==> OmniAuth | ||
| 221 | + # Add a new OmniAuth provider. Check the wiki for more information on setting | ||
| 222 | + # up on your models and hooks. | ||
| 223 | + # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' | ||
| 224 | + | ||
| 225 | + if Errbit::Config.github_authentication || Rails.env.test? | ||
| 226 | + config.omniauth :github, | ||
| 227 | + Errbit::Config.github_client_id, | ||
| 228 | + Errbit::Config.github_secret, | ||
| 229 | + :scope => Errbit::Config.github_access_scope.join(','), | ||
| 230 | + :skip_info => true | ||
| 231 | + end | ||
| 136 | 232 | ||
| 137 | # ==> Warden configuration | 233 | # ==> Warden configuration |
| 138 | - # If you want to use other strategies, that are not (yet) supported by Devise, | ||
| 139 | - # you can configure them inside the config.warden block. The example below | ||
| 140 | - # allows you to setup OAuth, using http://github.com/roman/warden_oauth | 234 | + # If you want to use other strategies, that are not supported by Devise, or |
| 235 | + # change the failure app, you can configure them inside the config.warden block. | ||
| 141 | # | 236 | # |
| 142 | # config.warden do |manager| | 237 | # config.warden do |manager| |
| 143 | - # manager.oauth(:twitter) do |twitter| | ||
| 144 | - # twitter.consumer_secret = <YOUR CONSUMER SECRET> | ||
| 145 | - # twitter.consumer_key = <YOUR CONSUMER KEY> | ||
| 146 | - # twitter.options :site => 'http://twitter.com' | ||
| 147 | - # end | ||
| 148 | - # manager.default_strategies(:scope => :user).unshift :twitter_oauth | 238 | + # manager.intercept_401 = false |
| 239 | + # manager.default_strategies(:scope => :user).unshift :some_external_strategy | ||
| 149 | # end | 240 | # end |
| 150 | -end | ||
| 151 | 241 | ||
| 242 | + # ==> Mountable engine configurations | ||
| 243 | + # When using Devise inside an engine, let's call it `MyEngine`, and this engine | ||
| 244 | + # is mountable, there are some extra configurations to be taken into account. | ||
| 245 | + # The following options are available, assuming the engine is mounted as: | ||
| 246 | + # | ||
| 247 | + # mount MyEngine, at: "/my_engine" | ||
| 248 | + # | ||
| 249 | + # The router that invoked `devise_for`, in the example above, would be: | ||
| 250 | + # config.router_name = :my_engine | ||
| 251 | + # | ||
| 252 | + # When using omniauth, Devise cannot automatically set Omniauth path, | ||
| 253 | + # so you need to do it manually. For the users scope, it would be: | ||
| 254 | + # config.omniauth_path_prefix = "/my_engine/users/auth" | ||
| 255 | +end |
config/initializers/inherited_resources.rb
config/initializers/mongo.rb
| 1 | -if mongo = ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL'] | ||
| 2 | - settings = URI.parse(mongo) | ||
| 3 | - database_name = settings.path.gsub(/^\//, '') | 1 | +# Some code extract from Mongoid gem |
| 2 | +config_file = Rails.root.join("config", "mongoid.yml") | ||
| 3 | +if config_file.file? && | ||
| 4 | + YAML.load(ERB.new(File.read(config_file)).result)[Rails.env].values.flatten.any? | ||
| 5 | + ::Mongoid.load!(config_file) | ||
| 6 | +elsif ENV['HEROKU'] || ENV['USE_ENV'] | ||
| 7 | + # No mongoid.yml file. Use ENV variable to define your MongoDB | ||
| 8 | + # configuration | ||
| 9 | + if mongo = ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL'] || ENV['MONGODB_URL'] | ||
| 10 | + settings = URI.parse(mongo) | ||
| 11 | + database_name = settings.path.gsub(/^\//, '') | ||
| 12 | + else | ||
| 13 | + settings = OpenStruct.new({ | ||
| 14 | + :host => ENV['MONGOID_HOST'], | ||
| 15 | + :port => ENV['MONGOID_PORT'], | ||
| 16 | + :user => ENV['MONGOID_USERNAME'], | ||
| 17 | + :password => ENV['MONGOID_PASSWORD'] | ||
| 18 | + }) | ||
| 19 | + database_name = ENV['MONGOID_DATABASE'] | ||
| 20 | + end | ||
| 4 | 21 | ||
| 5 | Mongoid.configure do |config| | 22 | Mongoid.configure do |config| |
| 6 | - config.master = Mongo::Connection.new(settings.host, settings.port).db(database_name) | ||
| 7 | - config.master.authenticate(settings.user, settings.password) if settings.user | ||
| 8 | - config.allow_dynamic_fields = false | ||
| 9 | - config.use_activesupport_time_zone = true | 23 | + |
| 24 | + hash = { | ||
| 25 | + sessions: { | ||
| 26 | + default: { | ||
| 27 | + database: database_name, | ||
| 28 | + hosts: [ "#{settings.host}:#{settings.port}" ] | ||
| 29 | + } | ||
| 30 | + }, | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + if settings.user && settings.password | ||
| 34 | + hash[:sessions][:default][:username] = settings.user | ||
| 35 | + hash[:sessions][:default][:password] = settings.password | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + config.load_configuration(hash) | ||
| 10 | end | 39 | end |
| 11 | end | 40 | end |
| 12 | 41 | ||
| 42 | +Mongoid.allow_dynamic_fields = false | ||
| 43 | +Mongoid.use_activesupport_time_zone = true | ||
| 44 | +Mongoid.identity_map_enabled = true |
config/initializers/secret_token.rb
| @@ -4,5 +4,33 @@ | @@ -4,5 +4,33 @@ | ||
| 4 | # If you change this key, all old signed cookies will become invalid! | 4 | # If you change this key, all old signed cookies will become invalid! |
| 5 | # Make sure the secret is at least 30 characters and all random, | 5 | # Make sure the secret is at least 30 characters and all random, |
| 6 | # no regular words or you'll be exposed to dictionary attacks. | 6 | # no regular words or you'll be exposed to dictionary attacks. |
| 7 | -Errbit::Application.config.secret_token = '6b74778101638fa9c156b3928c9492fb2481ab842538bea838d21f9c9993f649f5806449584266d413d0b2f1104162b3066a86512ed71ededd627cd41f939614' | ||
| 8 | 7 | ||
| 8 | +# Everyone can share the same token for development/test | ||
| 9 | +if %w(development test).include? Rails.env | ||
| 10 | + Errbit::Application.config.secret_token = 'f258ed69266dc8ad0ca79363c3d2f945c388a9c5920fc9a1ae99a98fbb619f135001c6434849b625884a9405a60cd3d50fc3e3b07ecd38cbed7406a4fccdb59c' | ||
| 11 | +else | ||
| 12 | + | ||
| 13 | + if ENV['SECRET_TOKEN'].present? | ||
| 14 | + Errbit::Application.config.secret_token = ENV['SECRET_TOKEN'] | ||
| 15 | + | ||
| 16 | + # Do not raise an error if secret token is not available during assets precompilation | ||
| 17 | + elsif ENV['RAILS_GROUPS'] != 'assets' | ||
| 18 | + raise <<-ERROR | ||
| 19 | + | ||
| 20 | + You must generate a unique secret token for your Errbit instance. | ||
| 21 | + | ||
| 22 | + If you are deploying via capistrano, please ensure that your `config/deploy.rb` contains | ||
| 23 | + the new `errbit:setup_configs` and `errbit:symlink_configs` tasks from `config/deploy.example.rb`. | ||
| 24 | + Next time you deploy, your secret token will be automatically generated. | ||
| 25 | + | ||
| 26 | + If you are deploying to Heroku, please run the following command to set your secret token: | ||
| 27 | + heroku config:add SECRET_TOKEN="$(bundle exec rake secret)" | ||
| 28 | + | ||
| 29 | + If you are deploying in some other way, please run the following command to generate a new secret token, | ||
| 30 | + and commit the new `config/initializers/secret_token.rb`: | ||
| 31 | + | ||
| 32 | + echo "Errbit::Application.config.secret_token = '$(bundle exec rake secret)'" > config/initializers/secret_token.rb | ||
| 33 | + | ||
| 34 | + ERROR | ||
| 35 | + end | ||
| 36 | +end |
config/initializers/ssl_enforcer.rb
| 1 | -# | ||
| 2 | # Enforce SSL connections, if configured | 1 | # Enforce SSL connections, if configured |
| 3 | if Errbit::Config.enforce_ssl | 2 | if Errbit::Config.enforce_ssl |
| 3 | + require 'rack/ssl-enforcer' | ||
| 4 | + ActionMailer::Base.default_url_options.merge!(:protocol => 'https://') | ||
| 4 | Errbit::Application.configure do | 5 | Errbit::Application.configure do |
| 5 | config.middleware.use Rack::SslEnforcer, :except => /^\/deploys/ | 6 | config.middleware.use Rack::SslEnforcer, :except => /^\/deploys/ |
| 6 | end | 7 | end |
config/locales/en.yml
| @@ -2,7 +2,6 @@ | @@ -2,7 +2,6 @@ | ||
| 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. | 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. |
| 3 | 3 | ||
| 4 | en: | 4 | en: |
| 5 | - hello: "Hello world" | ||
| 6 | flash: | 5 | flash: |
| 7 | apps: | 6 | apps: |
| 8 | create: | 7 | create: |
| @@ -11,6 +10,71 @@ en: | @@ -11,6 +10,71 @@ en: | ||
| 11 | success: "Good news everyone! '%{app_name}' was successfully updated." | 10 | success: "Good news everyone! '%{app_name}' was successfully updated." |
| 12 | destroy: | 11 | destroy: |
| 13 | success: "'%{app_name}' was successfully destroyed." | 12 | success: "'%{app_name}' was successfully destroyed." |
| 13 | + | ||
| 14 | n_errs_have: | 14 | n_errs_have: |
| 15 | one: "%{count} err has" | 15 | one: "%{count} err has" |
| 16 | other: "%{count} errs have" | 16 | other: "%{count} errs have" |
| 17 | + | ||
| 18 | + layouts: | ||
| 19 | + application: | ||
| 20 | + title: 'Errbit' | ||
| 21 | + errbit: 'Errbit' | ||
| 22 | + powered_html: "Powered by %{link} : the open source error catcher." | ||
| 23 | + | ||
| 24 | + shared: | ||
| 25 | + navigation: | ||
| 26 | + apps: 'Apps' | ||
| 27 | + errors: 'Errors' | ||
| 28 | + users: 'Users' | ||
| 29 | + session: | ||
| 30 | + sign_out: 'Sign out' | ||
| 31 | + edit_profile: 'Edit profile' | ||
| 32 | + | ||
| 33 | + controllers: | ||
| 34 | + apps: | ||
| 35 | + flash: | ||
| 36 | + create: | ||
| 37 | + success: "Your app was successfully created." | ||
| 38 | + error: "You app was successfully destroyed." | ||
| 39 | + update: | ||
| 40 | + success: "You app was successfully updated." | ||
| 41 | + error: "You app was not updated" | ||
| 42 | + destroy: | ||
| 43 | + success: "You app was successfully destroyed." | ||
| 44 | + error: "You app could not be destroyed." | ||
| 45 | + | ||
| 46 | + users: | ||
| 47 | + flash: | ||
| 48 | + destroy: | ||
| 49 | + success: "That's sad. %{name} is no longer part of your team." | ||
| 50 | + error: "You can't delete yourself" | ||
| 51 | + update: | ||
| 52 | + success: "%{name}'s information was successfully updated." | ||
| 53 | + problems: | ||
| 54 | + flash: | ||
| 55 | + no_select_problem: "You have not selected any errors" | ||
| 56 | + need_two_errors_merge: "You must select at least two errors to merge" | ||
| 57 | + merge_several: | ||
| 58 | + success: "%{nb} errors have been merged." | ||
| 59 | + | ||
| 60 | + devise: | ||
| 61 | + registrations: | ||
| 62 | + signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' | ||
| 63 | + signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' | ||
| 64 | + signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' | ||
| 65 | + omniauth_callbacks: | ||
| 66 | + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." | ||
| 67 | + success: "Successfully authenticated from %{kind} account." | ||
| 68 | + | ||
| 69 | + apps: | ||
| 70 | + index: | ||
| 71 | + notify: Notification Service | ||
| 72 | + tracker: Tracker | ||
| 73 | + last_deploy: Last Deploy | ||
| 74 | + errors: Errors | ||
| 75 | + name: Name | ||
| 76 | + repository: Repository | ||
| 77 | + title: Apps | ||
| 78 | + new_app: Add a New App | ||
| 79 | + no_apps: 'No apps here.' | ||
| 80 | + click_to_create: 'Click here to create your first one' |
config/mongoid.example.yml
| @@ -7,27 +7,34 @@ | @@ -7,27 +7,34 @@ | ||
| 7 | # `cap deploy:setup` is ran the first time. Be sure | 7 | # `cap deploy:setup` is ran the first time. Be sure |
| 8 | # to place production specific settings there | 8 | # to place production specific settings there |
| 9 | 9 | ||
| 10 | -defaults: &defaults | ||
| 11 | - host: localhost | ||
| 12 | - # slaves: | ||
| 13 | - # - host: slave1.local | ||
| 14 | - # port: 27018 | ||
| 15 | - # - host: slave2.local | ||
| 16 | - # port: 27019 | ||
| 17 | - | ||
| 18 | development: | 10 | development: |
| 19 | - <<: *defaults | ||
| 20 | - database: errbit_development | 11 | + sessions: |
| 12 | + default: | ||
| 13 | + database: errbit_development | ||
| 14 | + hosts: | ||
| 15 | + - localhost:27017 | ||
| 16 | + options: | ||
| 17 | + identity_map_enabled: true | ||
| 18 | + use_utc: true | ||
| 21 | 19 | ||
| 22 | test: | 20 | test: |
| 23 | - <<: *defaults | ||
| 24 | - database: errbit_test | 21 | + sessions: |
| 22 | + default: | ||
| 23 | + hosts: | ||
| 24 | + - localhost:27017 | ||
| 25 | + database: errbit_test | ||
| 26 | + options: | ||
| 27 | + identity_map_enabled: true | ||
| 28 | + use_utc: true | ||
| 25 | 29 | ||
| 26 | # set these environment variables on your prod server | 30 | # set these environment variables on your prod server |
| 27 | production: | 31 | production: |
| 28 | - <<: *defaults | ||
| 29 | - host: <%= ENV['MONGOID_HOST'] %> | ||
| 30 | - port: <%= ENV['MONGOID_PORT'] %> | ||
| 31 | - username: <%= ENV['MONGOID_USERNAME'] %> | ||
| 32 | - password: <%= ENV['MONGOID_PASSWORD'] %> | ||
| 33 | - database: <%= ENV['MONGOID_DATABASE'] %> | 32 | + sessions: |
| 33 | + default: | ||
| 34 | + database: <%= ENV['MONGOID_DATABASE'] %> | ||
| 35 | + hosts: | ||
| 36 | + - <%=ENV["MONGOID_HOST"]%><%=ENV["MONGOID_PORT"]%> | ||
| 37 | + username: <%= ENV['MONGOID_USERNAME'] %> | ||
| 38 | + password: <%= ENV['MONGOID_PASSWORD'] %> | ||
| 39 | + options: | ||
| 40 | + identity_map_enabled: true |
config/mongoid.mongohq.yml
config/mongoid.mongolab.yml
config/routes.rb
| @@ -21,7 +21,7 @@ Errbit::Application.routes.draw do | @@ -21,7 +21,7 @@ Errbit::Application.routes.draw do | ||
| 21 | post :unresolve_several | 21 | post :unresolve_several |
| 22 | post :merge_several | 22 | post :merge_several |
| 23 | post :unmerge_several | 23 | post :unmerge_several |
| 24 | - get :all | 24 | + get :search |
| 25 | end | 25 | end |
| 26 | end | 26 | end |
| 27 | 27 | ||
| @@ -44,7 +44,12 @@ Errbit::Application.routes.draw do | @@ -44,7 +44,12 @@ Errbit::Application.routes.draw do | ||
| 44 | namespace :api do | 44 | namespace :api do |
| 45 | namespace :v1 do | 45 | namespace :v1 do |
| 46 | resources :problems, :only => [:index], :defaults => { :format => 'json' } | 46 | resources :problems, :only => [:index], :defaults => { :format => 'json' } |
| 47 | - resources :notices, :only => [:index], :defaults => { :format => 'json' } | 47 | + resources :notices, :only => [:index], :defaults => { :format => 'json' } |
| 48 | + resources :stats, :only => [], :defaults => { :format => 'json' } do | ||
| 49 | + collection do | ||
| 50 | + get :app | ||
| 51 | + end | ||
| 52 | + end | ||
| 48 | end | 53 | end |
| 49 | end | 54 | end |
| 50 | 55 |
config/unicorn.rb
| @@ -3,3 +3,27 @@ | @@ -3,3 +3,27 @@ | ||
| 3 | worker_processes 3 # amount of unicorn workers to spin up | 3 | worker_processes 3 # amount of unicorn workers to spin up |
| 4 | timeout 30 # restarts workers that hang for 30 seconds | 4 | timeout 30 # restarts workers that hang for 30 seconds |
| 5 | preload_app true | 5 | preload_app true |
| 6 | + | ||
| 7 | +# Taken from github: https://github.com/blog/517-unicorn | ||
| 8 | +# Though everyone uses pretty miuch the same code | ||
| 9 | +before_fork do |server, worker| | ||
| 10 | + ## | ||
| 11 | + # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and | ||
| 12 | + # immediately start loading up a new version of itself (loaded with a new | ||
| 13 | + # version of our app). When this new Unicorn is completely loaded | ||
| 14 | + # it will begin spawning workers. The first worker spawned will check to | ||
| 15 | + # see if an .oldbin pidfile exists. If so, this means we've just booted up | ||
| 16 | + # a new Unicorn and need to tell the old one that it can now die. To do so | ||
| 17 | + # we send it a QUIT. | ||
| 18 | + # | ||
| 19 | + # Using this method we get 0 downtime deploys. | ||
| 20 | + | ||
| 21 | + old_pid = "#{server.config[:pid]}.oldbin" | ||
| 22 | + if File.exists?(old_pid) && server.pid != old_pid | ||
| 23 | + begin | ||
| 24 | + Process.kill("QUIT", File.read(old_pid).to_i) | ||
| 25 | + rescue Errno::ENOENT, Errno::ESRCH | ||
| 26 | + # someone else did our job for us | ||
| 27 | + end | ||
| 28 | + end | ||
| 29 | +end |
db/migrate/20110422152027_move_notices_to_separate_collection.rb
| 1 | class MoveNoticesToSeparateCollection < Mongoid::Migration | 1 | class MoveNoticesToSeparateCollection < Mongoid::Migration |
| 2 | def self.up | 2 | def self.up |
| 3 | + errs_coll = connection["errs"] | ||
| 4 | + | ||
| 3 | # copy embedded Notices into a separate collection | 5 | # copy embedded Notices into a separate collection |
| 4 | - mongo_db = Err.db | ||
| 5 | - errs = mongo_db.collection("errs").find({ }, :fields => ["notices"]) | 6 | + errs = errs_coll.find.select(notices: 1) |
| 6 | errs.each do |err| | 7 | errs.each do |err| |
| 7 | next unless err['notices'] | 8 | next unless err['notices'] |
| 8 | 9 | ||
| @@ -18,7 +19,7 @@ class MoveNoticesToSeparateCollection < Mongoid::Migration | @@ -18,7 +19,7 @@ class MoveNoticesToSeparateCollection < Mongoid::Migration | ||
| 18 | e.notices.create!(notice) | 19 | e.notices.create!(notice) |
| 19 | end | 20 | end |
| 20 | e.app.update_attribute(:notify_on_errs, old_notify) | 21 | e.app.update_attribute(:notify_on_errs, old_notify) |
| 21 | - mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}}) | 22 | + errs_coll.find({ "_id" => err['_id']}).update({ "$unset" => { "notices" => 1}}) |
| 22 | end | 23 | end |
| 23 | Rake::Task["errbit:db:update_notices_count"].invoke | 24 | Rake::Task["errbit:db:update_notices_count"].invoke |
| 24 | Rake::Task["errbit:db:update_problem_attrs"].invoke | 25 | Rake::Task["errbit:db:update_problem_attrs"].invoke |
db/migrate/20120530005915_rename_klass_to_error_class.rb
| 1 | class RenameKlassToErrorClass < Mongoid::Migration | 1 | class RenameKlassToErrorClass < Mongoid::Migration |
| 2 | def self.up | 2 | def self.up |
| 3 | [Problem, Err, Notice].each do |model| | 3 | [Problem, Err, Notice].each do |model| |
| 4 | - model.collection.update({}, {'$rename' => {'klass' => 'error_class'}}, :multi => true, :safe => true) | 4 | + model.collection.find.update({'$rename' => {'klass' => 'error_class'}}, :multi => true, :safe => true) |
| 5 | end | 5 | end |
| 6 | end | 6 | end |
| 7 | 7 | ||
| 8 | def self.down | 8 | def self.down |
| 9 | [Problem, Err, Notice].each do |model| | 9 | [Problem, Err, Notice].each do |model| |
| 10 | - model.collection.update({}, {'$rename' => {'error_class' => 'klass'}}, :multi => true, :safe => true) | 10 | + model.collection.find.update({'$rename' => {'error_class' => 'klass'}}, :multi => true, :safe => true) |
| 11 | end | 11 | end |
| 12 | end | 12 | end |
| 13 | end | 13 | end |
db/migrate/20120603112130_change_github_url_to_github_repo.rb
| 1 | class ChangeGithubUrlToGithubRepo < Mongoid::Migration | 1 | class ChangeGithubUrlToGithubRepo < Mongoid::Migration |
| 2 | def self.up | 2 | def self.up |
| 3 | - App.collection.update({}, {'$rename' => {'github_url' => 'github_repo'}}, :multi => true, :safe => true) | 3 | + App.collection.find.update({'$rename' => {'github_url' => 'github_repo'}}, :multi => true, :safe => true) |
| 4 | App.all.each do |app| | 4 | App.all.each do |app| |
| 5 | app.send :normalize_github_repo | 5 | app.send :normalize_github_repo |
| 6 | app.save | 6 | app.save |
| @@ -8,7 +8,7 @@ class ChangeGithubUrlToGithubRepo < Mongoid::Migration | @@ -8,7 +8,7 @@ class ChangeGithubUrlToGithubRepo < Mongoid::Migration | ||
| 8 | end | 8 | end |
| 9 | 9 | ||
| 10 | def self.down | 10 | def self.down |
| 11 | - App.collection.update({}, {'$rename' => {'github_repo' => 'github_url'}}, :multi => true, :safe => true) | 11 | + App.collection.find.update({'$rename' => {'github_repo' => 'github_url'}}, :multi => true, :safe => true) |
| 12 | App.all.each do |app| | 12 | App.all.each do |app| |
| 13 | unless app.github_repo.include?("github.com") | 13 | unless app.github_repo.include?("github.com") |
| 14 | app.update_attribute :github_url, "https://github.com/" << app.github_url | 14 | app.update_attribute :github_url, "https://github.com/" << app.github_url |
db/migrate/20121003223358_extract_backtraces.rb
| @@ -2,7 +2,8 @@ class ExtractBacktraces < Mongoid::Migration | @@ -2,7 +2,8 @@ class ExtractBacktraces < Mongoid::Migration | ||
| 2 | def self.up | 2 | def self.up |
| 3 | say "It could take long time (hours if you have many Notices)" | 3 | say "It could take long time (hours if you have many Notices)" |
| 4 | Notice.unscoped.all.each do |notice| | 4 | Notice.unscoped.all.each do |notice| |
| 5 | - backtrace = Backtrace.find_or_create(:raw => notice['backtrace']) | 5 | + next if notice.backtrace.present? || notice['backtrace'].nil? |
| 6 | + backtrace = Backtrace.find_or_create(:raw => notice['backtrace'] || []) | ||
| 6 | notice.backtrace = backtrace | 7 | notice.backtrace = backtrace |
| 7 | notice['backtrace'] = nil | 8 | notice['backtrace'] = nil |
| 8 | notice.save! | 9 | notice.save! |
| @@ -12,4 +13,4 @@ class ExtractBacktraces < Mongoid::Migration | @@ -12,4 +13,4 @@ class ExtractBacktraces < Mongoid::Migration | ||
| 12 | 13 | ||
| 13 | def self.down | 14 | def self.down |
| 14 | end | 15 | end |
| 15 | -end | 16 | -end |
| 17 | +end | ||
| 16 | \ No newline at end of file | 18 | \ No newline at end of file |
db/migrate/20121005142110_regenerate_err_fingerprints.rb
| 1 | class RegenerateErrFingerprints < Mongoid::Migration | 1 | class RegenerateErrFingerprints < Mongoid::Migration |
| 2 | def self.up | 2 | def self.up |
| 3 | Err.all.each do |err| | 3 | Err.all.each do |err| |
| 4 | - fingerprint_source = { | ||
| 5 | - :backtrace => err.notices.first.backtrace_id, | ||
| 6 | - :error_class => err.error_class, | ||
| 7 | - :component => err.component, | ||
| 8 | - :action => err.action, | ||
| 9 | - :environment => err.environment, | ||
| 10 | - :api_key => err.app.api_key | ||
| 11 | - } | ||
| 12 | - fingerprint = Digest::SHA1.hexdigest(fingerprint_source.to_s) | ||
| 13 | - err.update_attribute(:fingerprint, fingerprint) | 4 | + if err.notices.any? |
| 5 | + fingerprint_source = { | ||
| 6 | + :backtrace => err.notices.first.backtrace_id, | ||
| 7 | + :error_class => err.error_class, | ||
| 8 | + :component => err.component, | ||
| 9 | + :action => err.action, | ||
| 10 | + :environment => err.environment, | ||
| 11 | + :api_key => err.app.api_key | ||
| 12 | + } | ||
| 13 | + fingerprint = Digest::SHA1.hexdigest(fingerprint_source.to_s) | ||
| 14 | + err.update_attribute(:fingerprint, fingerprint) | ||
| 15 | + end | ||
| 14 | end | 16 | end |
| 15 | end | 17 | end |
| 16 | 18 |
db/migrate/20130208135718_allow_custom_xmpp_on_gtalk.rb.rb
0 → 100644
| @@ -0,0 +1,17 @@ | @@ -0,0 +1,17 @@ | ||
| 1 | +class AllowCustomXmppOnGtalk < Mongoid::Migration | ||
| 2 | + def self.up | ||
| 3 | + App.all.each do |app| | ||
| 4 | + if app.notification_service and app.notification_service._type.include?("Gtalk") | ||
| 5 | + user_id = app.notification_service.room_id | ||
| 6 | + app.notification_service.update_attributes(:service => 'talk.google.com', | ||
| 7 | + :service_url => "http://www.google.com/talk/", | ||
| 8 | + :user_id => user_id, | ||
| 9 | + :room_id => nil) | ||
| 10 | + | ||
| 11 | + end | ||
| 12 | + end | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + def self.down | ||
| 16 | + end | ||
| 17 | +end |
db/migrate/20130212112719_add_interval_field_for_notifications.rb
0 → 100644
| @@ -0,0 +1,12 @@ | @@ -0,0 +1,12 @@ | ||
| 1 | +class AddIntervalFieldForNotifications < Mongoid::Migration | ||
| 2 | + def self.up | ||
| 3 | + App.all.each do |app| | ||
| 4 | + if app.notification_service | ||
| 5 | + app.notification_service.update_attributes(:notify_at_notices => [0]) | ||
| 6 | + end | ||
| 7 | + end | ||
| 8 | + end | ||
| 9 | + | ||
| 10 | + def self.down | ||
| 11 | + end | ||
| 12 | +end |
db/seeds.rb
| @@ -12,12 +12,11 @@ puts "-- email: #{admin_email}" | @@ -12,12 +12,11 @@ puts "-- email: #{admin_email}" | ||
| 12 | puts "-- password: #{admin_pass}" | 12 | puts "-- password: #{admin_pass}" |
| 13 | puts "" | 13 | puts "" |
| 14 | puts "Be sure to change these credentials ASAP!" | 14 | puts "Be sure to change these credentials ASAP!" |
| 15 | -user = User.where(:email => admin_email).first || User.new({ | ||
| 16 | - :name => 'Errbit Admin', | ||
| 17 | - :email => admin_email, | ||
| 18 | - :password => admin_pass, | ||
| 19 | - :password_confirmation => admin_pass | ||
| 20 | -}) | 15 | +user = User.find_or_initialize_by(:email => admin_email) do |u| |
| 16 | + u.name = 'Errbit Admin' | ||
| 17 | + u.password = admin_pass | ||
| 18 | + u.password_confirmation = admin_pass | ||
| 19 | +end | ||
| 21 | user.username = admin_username if Errbit::Config.user_has_username | 20 | user.username = admin_username if Errbit::Config.user_has_username |
| 22 | 21 | ||
| 23 | user.admin = true | 22 | user.admin = true |
| @@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
| 1 | +# Some Tips to help you when you develop on Errbit | ||
| 2 | + | ||
| 3 | +## Running spec on multi-threaded mode | ||
| 4 | + | ||
| 5 | +Running the complete test suite can be really long. You can running it | ||
| 6 | +on multi-fork system with the wonderfull gem of | ||
| 7 | +[@tmm1](http://github.com/tmm1), [test-queue](http://github.com/tmm1/test-queue) | ||
| 8 | + | ||
| 9 | +If you want do it, you need install in first the gem 'test-queue' | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +gem install test-queue | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +After you just need launch the script with adapting runner of mongoid. | ||
| 16 | + | ||
| 17 | +``` | ||
| 18 | +./script/rspec-queue-mongoid.rb spec | ||
| 19 | +``` | ||
| 20 | + | ||
| 21 | +In my case, the complete test suite down to 2min after a 16min long | ||
| 22 | +before. |
| @@ -0,0 +1,77 @@ | @@ -0,0 +1,77 @@ | ||
| 1 | +# Which env variable you can use ? | ||
| 2 | + | ||
| 3 | +Errbit can be almost configured by some ENVIRONMENT variables. If you | ||
| 4 | +use this variable, you don't need copy all of you configuration file | ||
| 5 | + | ||
| 6 | +To activate this env variable you need activate it by a Variable env. | ||
| 7 | +You can do that with HEROKU or USE_ENV variable | ||
| 8 | + | ||
| 9 | +If you activate it you can use all of this env variable : | ||
| 10 | + | ||
| 11 | +## Errbit base configuration | ||
| 12 | + | ||
| 13 | +* ERRBIT_HOST : the host of your errbit instance (not define by default) | ||
| 14 | +* ERRBIT_EMAIL_FROM : the email sending all of your notification (not | ||
| 15 | + define by default ) | ||
| 16 | +* ERRBIT_CONFIRM_RESOLVE_ERR : define if you need confirm when you mark | ||
| 17 | + a problem resolve. ( true by default, fill it and you not need | ||
| 18 | +confirm ) | ||
| 19 | +* ERRBIT_USER_HAS_USERNAME : allow identify your user by username | ||
| 20 | + instead of email. ( false by default, set to '1' to activate it) | ||
| 21 | +* ERRBIT_ALLOW_COMMENTS_WITH_ISSUE_TRACKER : define if you activate the | ||
| 22 | + comment or not. By default comment are | ||
| 23 | +* ERRBIT_ENFORCE_SSL : allow force the ssl on all the application. By | ||
| 24 | + default is false | ||
| 25 | +* ERRBIT_USE_GRAVATAR : allow use gravatar to see user gravatar in user | ||
| 26 | + comment and page | ||
| 27 | + | ||
| 28 | +## Authentification configuration | ||
| 29 | + | ||
| 30 | +Environement variable allow define how you can auth on your errbit | ||
| 31 | + | ||
| 32 | +### Github authentification | ||
| 33 | + | ||
| 34 | +You can allow the GITHUB auth | ||
| 35 | + | ||
| 36 | +* GITHUB_AUTHENTIFICATION : define if you allow the github auth. By | ||
| 37 | + default false | ||
| 38 | +* GITHUB_CLIENT_ID : you github app client id to use in your github auth | ||
| 39 | +* GITHUB_SECRET : your github app secret to use in your github auth | ||
| 40 | +* GITHUB_ACCESS_SCOPE : The scope to ask to access on github account | ||
| 41 | + | ||
| 42 | +## Email sending configuration | ||
| 43 | + | ||
| 44 | +You can define how you connect your email sending system By all of this | ||
| 45 | +information. All mail can be send only by SMTP if you use variable | ||
| 46 | +system | ||
| 47 | + | ||
| 48 | +* SMTP_SERVER | ||
| 49 | +* SMTP_PORT | ||
| 50 | +* SMTP_USERNAME | ||
| 51 | +* SMTP_PASSWORD | ||
| 52 | +* SMTP_DOMAIN | ||
| 53 | + | ||
| 54 | +## MongoDB | ||
| 55 | + | ||
| 56 | +You can define your MongoDB connection by 2 ways. If you have an URL, | ||
| 57 | +you can define one of this ENV variables. All independently can works | ||
| 58 | + | ||
| 59 | +* MONGOLAB_URI | ||
| 60 | +* MONGOHQ_URL | ||
| 61 | + | ||
| 62 | +If you have a complete MongoDB connection you can define it by all | ||
| 63 | +information associate to your MongoDB connection. You need define all | ||
| 64 | +variable. | ||
| 65 | + | ||
| 66 | +* MONGOID_HOST | ||
| 67 | +* MONGOID_PORT | ||
| 68 | +* MONGOID_USERNAME | ||
| 69 | +* MONGOID_PASSWORD | ||
| 70 | +* MONGOID_DATABASE | ||
| 71 | + | ||
| 72 | +## Flowdock notification adapter | ||
| 73 | + | ||
| 74 | +If you noticed default Gravatar icon in your Flowdock notifications you | ||
| 75 | +may want to [add Errbit icon](http://gravatar.com) for email that is | ||
| 76 | +set in ERRBIT_EMAIL_FROM. | ||
| 77 | +You don't need to approve or authorize it on Flowdock because it is used only for an icon. |
45.1 KB
| @@ -0,0 +1,21 @@ | @@ -0,0 +1,21 @@ | ||
| 1 | +# Flowdock | ||
| 2 | + | ||
| 3 | +The flowdock notification send to [flowdock](https://www.flowdock.com/). | ||
| 4 | + | ||
| 5 | +## Configuration | ||
| 6 | + | ||
| 7 | +### Api token | ||
| 8 | + | ||
| 9 | +You need define the API token. | ||
| 10 | +It is purposely called Flow API Token and not Flowdock API Token to make | ||
| 11 | +it more obvious for user which one of the two tokens we expect: | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +### Sender ( not configure ) | ||
| 16 | + | ||
| 17 | +If you noticed default Gravatar icon in your Flowdock notifications you | ||
| 18 | +may want to [add Errbit icon](http://gravatar.com) for email that is | ||
| 19 | +set in ERRBIT_EMAIL_FROM. | ||
| 20 | +You don't need to approve or authorize it on Flowdock because it is | ||
| 21 | +used only for an icon. |
lib/hoptoad.rb
| @@ -3,7 +3,7 @@ require 'hoptoad/v2' | @@ -3,7 +3,7 @@ require 'hoptoad/v2' | ||
| 3 | module Hoptoad | 3 | module Hoptoad |
| 4 | class ApiVersionError < StandardError | 4 | class ApiVersionError < StandardError |
| 5 | def initialize | 5 | def initialize |
| 6 | - super "Wrong API Version: Expecting 2.0, 2.1, 2.2 or 2.3" | 6 | + super "Wrong API Version: Expecting 2.0, 2.1, 2.2, 2.3 or 2.4" |
| 7 | end | 7 | end |
| 8 | end | 8 | end |
| 9 | 9 | ||
| @@ -16,7 +16,7 @@ module Hoptoad | @@ -16,7 +16,7 @@ module Hoptoad | ||
| 16 | private | 16 | private |
| 17 | def self.get_version_processor(version) | 17 | def self.get_version_processor(version) |
| 18 | case version | 18 | case version |
| 19 | - when /2\.[0123]/; Hoptoad::V2 | 19 | + when /2\.[01234]/; Hoptoad::V2 |
| 20 | else; raise ApiVersionError | 20 | else; raise ApiVersionError |
| 21 | end | 21 | end |
| 22 | end | 22 | end |
lib/hoptoad/v2.rb
| @@ -18,6 +18,8 @@ module Hoptoad | @@ -18,6 +18,8 @@ module Hoptoad | ||
| 18 | {normalize_key(node['key']) => rekey(node['__content__'])} | 18 | {normalize_key(node['key']) => rekey(node['__content__'])} |
| 19 | elsif node.has_key?('__content__') | 19 | elsif node.has_key?('__content__') |
| 20 | rekey(node['__content__']) | 20 | rekey(node['__content__']) |
| 21 | + elsif node.has_key?('key') | ||
| 22 | + {normalize_key(node['key']) => nil} | ||
| 21 | else | 23 | else |
| 22 | node.inject({}) {|rekeyed, (key, val)| rekeyed.merge(normalize_key(key) => rekey(val))} | 24 | node.inject({}) {|rekeyed, (key, val)| rekeyed.merge(normalize_key(key) => rekey(val))} |
| 23 | end | 25 | end |
| @@ -59,10 +61,10 @@ module Hoptoad | @@ -59,10 +61,10 @@ module Hoptoad | ||
| 59 | 61 | ||
| 60 | :api_key => notice['api-key'], | 62 | :api_key => notice['api-key'], |
| 61 | :notifier => notice['notifier'], | 63 | :notifier => notice['notifier'], |
| 62 | - :user_attributes => notice['user-attributes'] || {}, | ||
| 63 | - :current_user => notice['current-user'] || {} | 64 | + # 'current-user' from airbrake, 'user-attributes' from airbrake_user_attributes gem |
| 65 | + :user_attributes => notice['current-user'] || notice['user-attributes'] || {}, | ||
| 66 | + :framework => notice['framework'] | ||
| 64 | } | 67 | } |
| 65 | end | 68 | end |
| 66 | end | 69 | end |
| 67 | end | 70 | end |
| 68 | - |
lib/tasks/errbit/database.rake
| @@ -6,7 +6,9 @@ namespace :errbit do | @@ -6,7 +6,9 @@ namespace :errbit do | ||
| 6 | desc "Updates cached attributes on Problem" | 6 | desc "Updates cached attributes on Problem" |
| 7 | task :update_problem_attrs => :environment do | 7 | task :update_problem_attrs => :environment do |
| 8 | puts "Updating problems" | 8 | puts "Updating problems" |
| 9 | - Problem.all.each(&:cache_notice_attributes) | 9 | + Problem.all.each{|problem| |
| 10 | + ProblemUpdaterCache.new(problem).update | ||
| 11 | + } | ||
| 10 | end | 12 | end |
| 11 | 13 | ||
| 12 | desc "Updates Problem#notices_count" | 14 | desc "Updates Problem#notices_count" |
| @@ -19,9 +21,8 @@ namespace :errbit do | @@ -19,9 +21,8 @@ namespace :errbit do | ||
| 19 | 21 | ||
| 20 | desc "Delete resolved errors from the database. (Useful for limited heroku databases)" | 22 | desc "Delete resolved errors from the database. (Useful for limited heroku databases)" |
| 21 | task :clear_resolved => :environment do | 23 | task :clear_resolved => :environment do |
| 22 | - count = Problem.resolved.count | ||
| 23 | - Problem.resolved.each {|problem| problem.destroy } | ||
| 24 | - puts "=== Cleared #{count} resolved errors from the database." if count > 0 | 24 | + require 'resolved_problem_clearer' |
| 25 | + puts "=== Cleared #{ResolvedProblemClearer.new.execute} resolved errors from the database." | ||
| 25 | end | 26 | end |
| 26 | 27 | ||
| 27 | desc "Regenerate fingerprints" | 28 | desc "Regenerate fingerprints" |
lib/tasks/errbit/demo.rake
| @@ -42,15 +42,15 @@ namespace :errbit do | @@ -42,15 +42,15 @@ namespace :errbit do | ||
| 42 | 42 | ||
| 43 | errors.each do |error_template| | 43 | errors.each do |error_template| |
| 44 | rand(34).times do | 44 | rand(34).times do |
| 45 | - | ||
| 46 | - error_report = error_template.reverse_merge({ | 45 | + ErrorReport.new(error_template.reverse_merge({ |
| 46 | + :api_key => app.api_key, | ||
| 47 | :error_class => "StandardError", | 47 | :error_class => "StandardError", |
| 48 | :message => "Oops. Something went wrong!", | 48 | :message => "Oops. Something went wrong!", |
| 49 | :backtrace => random_backtrace, | 49 | :backtrace => random_backtrace, |
| 50 | :request => { | 50 | :request => { |
| 51 | - 'component' => 'main', | ||
| 52 | - 'action' => 'error' | ||
| 53 | - }, | 51 | + 'component' => 'main', |
| 52 | + 'action' => 'error' | ||
| 53 | + }, | ||
| 54 | :server_environment => {'environment-name' => Rails.env.to_s}, | 54 | :server_environment => {'environment-name' => Rails.env.to_s}, |
| 55 | :notifier => {:name => "seeds.rb"}, | 55 | :notifier => {:name => "seeds.rb"}, |
| 56 | :app_user => { | 56 | :app_user => { |
| @@ -59,9 +59,7 @@ namespace :errbit do | @@ -59,9 +59,7 @@ namespace :errbit do | ||
| 59 | :name => "John Smith", | 59 | :name => "John Smith", |
| 60 | :url => "http://www.example.com/users/jsmith" | 60 | :url => "http://www.example.com/users/jsmith" |
| 61 | } | 61 | } |
| 62 | - }) | ||
| 63 | - | ||
| 64 | - app.report_error!(error_report) | 62 | + })).generate_notice! |
| 65 | end | 63 | end |
| 66 | end | 64 | end |
| 67 | 65 |
public/javascripts/notifier.js
| 1 | -var Hoptoad = { | ||
| 2 | - VERSION : '2.0', | ||
| 3 | - NOTICE_XML : '<?xml version="1.0" encoding="UTF-8"?>\ | ||
| 4 | - <notice version="2.0">\ | ||
| 5 | - <api-key></api-key>\ | ||
| 6 | - <notifier>\ | ||
| 7 | - <name>errbit_notifier_js</name>\ | ||
| 8 | - <version>2.0</version>\ | ||
| 9 | - <url>https://github.com/errbit/errbit</url>\ | ||
| 10 | - </notifier>\ | ||
| 11 | - <error>\ | ||
| 12 | - <class>EXCEPTION_CLASS</class>\ | ||
| 13 | - <message>EXCEPTION_MESSAGE</message>\ | ||
| 14 | - <backtrace>BACKTRACE_LINES</backtrace>\ | ||
| 15 | - </error>\ | ||
| 16 | - <request>\ | ||
| 17 | - <url>REQUEST_URL</url>\ | ||
| 18 | - <component>REQUEST_COMPONENT</component>\ | ||
| 19 | - <action>REQUEST_ACTION</action>\ | ||
| 20 | - </request>\ | ||
| 21 | - <server-environment>\ | ||
| 22 | - <project-root>PROJECT_ROOT</project-root>\ | ||
| 23 | - <environment-name>production</environment-name>\ | ||
| 24 | - </server-environment>\ | ||
| 25 | - </notice>', | ||
| 26 | - ROOT : window.location.protocol + '//' + window.location.host, | ||
| 27 | - BACKTRACE_MATCHER : /^(.*)\@(.*)\:(\d+)$/, | ||
| 28 | - backtrace_filters : [/notifier\.js/], | ||
| 29 | - | ||
| 30 | - notify: function(error) { | ||
| 31 | - var xml = escape(Hoptoad.generateXML(error)); | ||
| 32 | - var host = Hoptoad.host; | ||
| 33 | - var url = '//' + host + '/notifier_api/v2/notices.xml?data=' + xml; | ||
| 34 | - var request = document.createElement('iframe'); | ||
| 35 | - | ||
| 36 | - request.style.width = '1px'; | ||
| 37 | - request.style.height = '1px'; | ||
| 38 | - request.style.display = 'none'; | ||
| 39 | - request.src = url; | ||
| 40 | - | ||
| 41 | - document.getElementsByTagName('head')[0].appendChild(request); | ||
| 42 | - }, | ||
| 43 | - | ||
| 44 | - setEnvironment: function(value) { | ||
| 45 | - var matcher = /<environment-name>.*<\/environment-name>/; | ||
| 46 | - | ||
| 47 | - Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher, | ||
| 48 | - '<environment-name>' + | ||
| 49 | - value + | ||
| 50 | - '</environment-name>') | ||
| 51 | - }, | ||
| 52 | - | ||
| 53 | - setHost: function(value) { | ||
| 54 | - Hoptoad.host = value; | ||
| 55 | - }, | ||
| 56 | - | ||
| 57 | - setKey: function(value) { | ||
| 58 | - var matcher = /<api-key>.*<\/api-key>/; | ||
| 59 | - | ||
| 60 | - Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher, | ||
| 61 | - '<api-key>' + | ||
| 62 | - value + | ||
| 63 | - '</api-key>'); | ||
| 64 | - }, | ||
| 65 | - | ||
| 66 | - setErrorDefaults: function(value) { | ||
| 67 | - Hoptoad.errorDefaults = value; | ||
| 68 | - }, | ||
| 69 | - | ||
| 70 | - generateXML: function(errorWithoutDefaults) { | ||
| 71 | - var error = Hoptoad.mergeDefault(Hoptoad.errorDefaults, errorWithoutDefaults); | ||
| 72 | - | ||
| 73 | - var xml = Hoptoad.NOTICE_XML; | ||
| 74 | - var url = Hoptoad.escapeText(error.url || ''); | ||
| 75 | - var component = Hoptoad.escapeText(error.component || ''); | ||
| 76 | - var action = Hoptoad.escapeText(error.action || ''); | ||
| 77 | - var type = Hoptoad.escapeText(error.type || 'Error'); | ||
| 78 | - var message = Hoptoad.escapeText(error.message || 'Unknown error.'); | ||
| 79 | - var backtrace = Hoptoad.generateBacktrace(error); | ||
| 80 | - | ||
| 81 | - | ||
| 82 | - if (Hoptoad.trim(url) == '' && Hoptoad.trim(component) == '') { | ||
| 83 | - xml = xml.replace(/<request>.*<\/request>/, ''); | ||
| 84 | - } else { | ||
| 85 | - var data = ''; | ||
| 86 | - | ||
| 87 | - var cgi_data = error['cgi-data'] || {}; | ||
| 88 | - cgi_data["HTTP_USER_AGENT"] = navigator.userAgent; | ||
| 89 | - data += '<cgi-data>'; | ||
| 90 | - data += Hoptoad.generateVariables(cgi_data); | ||
| 91 | - data += '</cgi-data>'; | ||
| 92 | - | ||
| 93 | - var methods = ['params', 'session']; | ||
| 94 | - | ||
| 95 | - for (var i = 0; i < methods.length; i++) { | ||
| 96 | - var method = methods[i]; | ||
| 97 | - | ||
| 98 | - if (error[method]) { | ||
| 99 | - data += '<' + method + '>'; | ||
| 100 | - data += Hoptoad.generateVariables(error[method]); | ||
| 101 | - data += '</' + method + '>'; | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | - | ||
| 105 | - xml = xml.replace('</request>', data + '</request>') | ||
| 106 | - .replace('REQUEST_URL', url) | ||
| 107 | - .replace('REQUEST_ACTION', action) | ||
| 108 | - .replace('REQUEST_COMPONENT', component); | ||
| 109 | - } | ||
| 110 | - | ||
| 111 | - return xml.replace('PROJECT_ROOT', Hoptoad.ROOT) | ||
| 112 | - .replace('EXCEPTION_CLASS', type) | ||
| 113 | - .replace('EXCEPTION_MESSAGE', message) | ||
| 114 | - .replace('BACKTRACE_LINES', backtrace.join('')); | ||
| 115 | - }, | ||
| 116 | - | ||
| 117 | - generateBacktrace: function(error) { | ||
| 118 | - error = error || {}; | ||
| 119 | - | ||
| 120 | - if (typeof error.stack != 'string') { | ||
| 121 | - try { | ||
| 122 | - (0)(); | ||
| 123 | - } catch(e) { | ||
| 124 | - error.stack = e.stack; | ||
| 125 | - } | ||
| 126 | - } | ||
| 127 | - | ||
| 128 | - var backtrace = []; | ||
| 129 | - var stacktrace = Hoptoad.getStackTrace(error); | ||
| 130 | - | ||
| 131 | - for (var i = 0, l = stacktrace.length; i < l; i++) { | ||
| 132 | - var line = stacktrace[i]; | ||
| 133 | - var matches = line.match(Hoptoad.BACKTRACE_MATCHER); | ||
| 134 | - | ||
| 135 | - if (matches && Hoptoad.validBacktraceLine(line)) { | ||
| 136 | - var file = matches[2].replace(Hoptoad.ROOT, '[PROJECT_ROOT]'); | ||
| 137 | - | ||
| 138 | - if (i == 0) { | ||
| 139 | - if (matches[2].match(document.location.href)) { | ||
| 140 | - backtrace.push('<line method="" file="internal: " number=""/>'); | ||
| 141 | - } | ||
| 142 | - } | ||
| 143 | - | ||
| 144 | - backtrace.push('<line method="' + Hoptoad.escapeText(matches[1]) + | ||
| 145 | - '" file="' + Hoptoad.escapeText(file) + | ||
| 146 | - '" number="' + matches[3] + '" />'); | ||
| 147 | - } | ||
| 148 | - } | ||
| 149 | - | ||
| 150 | - return backtrace; | ||
| 151 | - }, | ||
| 152 | - | ||
| 153 | - getStackTrace: function(error) { | ||
| 154 | - var stacktrace = printStackTrace({ e : error, guess : false }); | ||
| 155 | - | ||
| 156 | - for (var i = 0, l = stacktrace.length; i < l; i++) { | ||
| 157 | - if (stacktrace[i].match(/\:\d+$/)) { | ||
| 158 | - continue; | ||
| 159 | - } | ||
| 160 | - | ||
| 161 | - if (stacktrace[i].indexOf('@') == -1) { | ||
| 162 | - stacktrace[i] += '@unsupported.js'; | ||
| 163 | - } | ||
| 164 | - | ||
| 165 | - stacktrace[i] += ':0'; | ||
| 166 | - } | ||
| 167 | - | ||
| 168 | - return stacktrace; | ||
| 169 | - }, | ||
| 170 | - | ||
| 171 | - validBacktraceLine: function(line) { | ||
| 172 | - for (var i = 0; i < Hoptoad.backtrace_filters.length; i++) { | ||
| 173 | - if (line.match(Hoptoad.backtrace_filters[i])) { | ||
| 174 | - return false; | ||
| 175 | - } | ||
| 176 | - } | ||
| 177 | - | ||
| 178 | - return true; | ||
| 179 | - }, | ||
| 180 | - | ||
| 181 | - generateVariables: function(parameters) { | ||
| 182 | - var key; | ||
| 183 | - var result = ''; | ||
| 184 | - | ||
| 185 | - for (key in parameters) { | ||
| 186 | - result += '<var key="' + Hoptoad.escapeText(key) + '">' + | ||
| 187 | - Hoptoad.escapeText(parameters[key]) + | ||
| 188 | - '</var>'; | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - return result; | ||
| 192 | - }, | ||
| 193 | - | ||
| 194 | - escapeText: function(text) { | ||
| 195 | - return text.replace(/&/g, '&') | ||
| 196 | - .replace(/</g, '<') | ||
| 197 | - .replace(/>/g, '>') | ||
| 198 | - .replace(/'/g, ''') | ||
| 199 | - .replace(/"/g, '"'); | ||
| 200 | - }, | ||
| 201 | - | ||
| 202 | - trim: function(text) { | ||
| 203 | - return text.toString().replace(/^\s+/, '').replace(/\s+$/, ''); | ||
| 204 | - }, | ||
| 205 | - | ||
| 206 | - mergeDefault: function(defaults, hash) { | ||
| 207 | - var cloned = {}; | ||
| 208 | - var key; | ||
| 209 | - | ||
| 210 | - for (key in hash) { | ||
| 211 | - cloned[key] = hash[key]; | ||
| 212 | - } | ||
| 213 | - | ||
| 214 | - for (key in defaults) { | ||
| 215 | - if (!cloned.hasOwnProperty(key)) { | ||
| 216 | - cloned[key] = defaults[key]; | ||
| 217 | - } | ||
| 218 | - } | ||
| 219 | - | ||
| 220 | - return cloned; | ||
| 221 | - } | ||
| 222 | -}; | ||
| 223 | - | ||
| 224 | - | ||
| 225 | - | ||
| 226 | - | 1 | +// Airbrake JavaScript Notifier Bundle |
| 2 | +(function(window, document, undefined) { | ||
| 227 | // Domain Public by Eric Wendelin http://eriwen.com/ (2008) | 3 | // Domain Public by Eric Wendelin http://eriwen.com/ (2008) |
| 228 | -// Luke Smith http://lucassmith.name/ (2008) | ||
| 229 | -// Loic Dachary <loic@dachary.org> (2008) | ||
| 230 | -// Johan Euphrosine <proppy@aminche.com> (2008) | ||
| 231 | -// Øyvind Sean Kinsey http://kinsey.no/blog (2010) | 4 | +// Luke Smith http://lucassmith.name/ (2008) |
| 5 | +// Loic Dachary <loic@dachary.org> (2008) | ||
| 6 | +// Johan Euphrosine <proppy@aminche.com> (2008) | ||
| 7 | +// Øyvind Sean Kinsey http://kinsey.no/blog (2010) | ||
| 8 | +// Victor Homyakov (2010) | ||
| 232 | // | 9 | // |
| 233 | // Information and discussions | 10 | // Information and discussions |
| 234 | // http://jspoker.pokersource.info/skin/test-printstacktrace.html | 11 | // http://jspoker.pokersource.info/skin/test-printstacktrace.html |
| 235 | // http://eriwen.com/javascript/js-stack-trace/ | 12 | // http://eriwen.com/javascript/js-stack-trace/ |
| 236 | // http://eriwen.com/javascript/stacktrace-update/ | 13 | // http://eriwen.com/javascript/stacktrace-update/ |
| 237 | // http://pastie.org/253058 | 14 | // http://pastie.org/253058 |
| 238 | -// http://browsershots.org/http://jspoker.pokersource.info/skin/test-printstacktrace.html | ||
| 239 | -// | ||
| 240 | // | 15 | // |
| 241 | // guessFunctionNameFromLines comes from firebug | 16 | // guessFunctionNameFromLines comes from firebug |
| 242 | // | 17 | // |
| @@ -270,24 +45,1135 @@ var Hoptoad = { | @@ -270,24 +45,1135 @@ var Hoptoad = { | ||
| 270 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | 45 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER |
| 271 | // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT | 46 | // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT |
| 272 | // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 47 | // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 273 | -function printStackTrace(a){var b=a&&a.e?a.e:null;a=a?!!a.guess:true;var c=new printStackTrace.implementation;b=c.run(b);return a?c.guessFunctions(b):b}printStackTrace.implementation=function(){}; | ||
| 274 | -printStackTrace.implementation.prototype={run:function(a){var b=this._mode||this.mode();if(b==="other")return this.other(arguments.callee);else{var c;if(!(c=a))a:{try{0()}catch(d){c=d;break a}c=void 0}a=c;return this[b](a)}},mode:function(){try{0()}catch(a){if(a.arguments)return this._mode="chrome";else if(a.stack)return this._mode="firefox";else if(window.opera&&!("stacktrace"in a))return this._mode="opera"}return this._mode="other"},chrome:function(a){return a.stack.replace(/^.*?\n/,"").replace(/^.*?\n/, | ||
| 275 | -"").replace(/^.*?\n/,"").replace(/^[^\(]+?[\n$]/gm,"").replace(/^\s+at\s+/gm,"").replace(/^Object.<anonymous>\s*\(/gm,"{anonymous}()@").split("\n")},firefox:function(a){return a.stack.replace(/^.*?\n/,"").replace(/(?:\n@:0)?\s+$/m,"").replace(/^\(/gm,"{anonymous}(").split("\n")},opera:function(a){a=a.message.split("\n");var b=/Line\s+(\d+).*?script\s+(http\S+)(?:.*?in\s+function\s+(\S+))?/i,c,d,e;c=4;d=0;for(e=a.length;c<e;c+=2)if(b.test(a[c]))a[d++]=(RegExp.$3?RegExp.$3+"()@"+RegExp.$2+RegExp.$1: | ||
| 276 | -"{anonymous}()@"+RegExp.$2+":"+RegExp.$1)+" -- "+a[c+1].replace(/^\s+/,"");a.splice(d,a.length-d);return a},other:function(a){for(var b=/function\s*([\w\-$]+)?\s*\(/i,c=[],d=0,e,f;a&&c.length<10;){e=b.test(a.toString())?RegExp.$1||"{anonymous}":"{anonymous}";f=Array.prototype.slice.call(a.arguments);c[d++]=e+"("+printStackTrace.implementation.prototype.stringifyArguments(f)+")";if(a===a.caller&&window.opera)break;a=a.caller}return c},stringifyArguments:function(a){for(var b=0;b<a.length;++b){var c= | ||
| 277 | -a[b];if(typeof c=="object")a[b]="#object";else if(typeof c=="function")a[b]="#function";else if(typeof c=="string")a[b]='"'+c+'"'}return a.join(",")},sourceCache:{},ajax:function(a){var b=this.createXMLHTTPObject();if(b){b.open("GET",a,false);b.setRequestHeader("User-Agent","XMLHTTP/1.0");b.send("");return b.responseText}},createXMLHTTPObject:function(){for(var a,b=[function(){return new XMLHttpRequest},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml3.XMLHTTP")}, | ||
| 278 | -function(){return new ActiveXObject("Microsoft.XMLHTTP")}],c=0;c<b.length;c++)try{a=b[c]();this.createXMLHTTPObject=b[c];return a}catch(d){}},getSource:function(a){a in this.sourceCache||(this.sourceCache[a]=this.ajax(a).split("\n"));return this.sourceCache[a]},guessFunctions:function(a){for(var b=0;b<a.length;++b){var c=a[b],d=/{anonymous}\(.*\)@(\w+:\/\/([-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/.exec(c);if(d){var e=d[1];d=d[4];if(e&&d){e=this.guessFunctionName(e,d);a[b]=c.replace("{anonymous}",e)}}}return a}, | ||
| 279 | -guessFunctionName:function(a,b){try{return this.guessFunctionNameFromLines(b,this.getSource(a))}catch(c){return"getSource failed with url: "+a+", exception: "+c.toString()}},guessFunctionNameFromLines:function(a,b){for(var c=/function ([^(]*)\(([^)]*)\)/,d=/['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/,e="",f=0;f<10;++f){e=b[a-f]+e;if(e!==undefined){var g=d.exec(e);if(g&&g[1])return g[1];else if((g=c.exec(e))&&g[1])return g[1]}}return"(?)"}}; | ||
| 280 | 48 | ||
| 49 | +/** | ||
| 50 | + * Main function giving a function stack trace with a forced or passed in Error | ||
| 51 | + * | ||
| 52 | + * @cfg {Error} e The error to create a stacktrace from (optional) | ||
| 53 | + * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions | ||
| 54 | + * @return {Array} of Strings with functions, lines, files, and arguments where possible | ||
| 55 | + */ | ||
| 56 | +function printStackTrace(options) { | ||
| 57 | + options = options || {guess: true}; | ||
| 58 | + var ex = options.e || null, guess = !!options.guess; | ||
| 59 | + var p = new printStackTrace.implementation(), result = p.run(ex); | ||
| 60 | + return (guess) ? p.guessAnonymousFunctions(result) : result; | ||
| 61 | +} | ||
| 281 | 62 | ||
| 63 | +if (typeof module !== "undefined" && module.exports) { | ||
| 64 | + module.exports = printStackTrace; | ||
| 65 | +} | ||
| 282 | 66 | ||
| 283 | - | ||
| 284 | -window.onerror = function(message, file, line) { | ||
| 285 | - setTimeout(function() { | ||
| 286 | - Hoptoad.notify({ | ||
| 287 | - message : message, | ||
| 288 | - stack : '()@' + file + ':' + line | ||
| 289 | - }); | ||
| 290 | - }, 100); | ||
| 291 | - return true; | 67 | +printStackTrace.implementation = function() { |
| 292 | }; | 68 | }; |
| 293 | 69 | ||
| 70 | +printStackTrace.implementation.prototype = { | ||
| 71 | + /** | ||
| 72 | + * @param {Error} ex The error to create a stacktrace from (optional) | ||
| 73 | + * @param {String} mode Forced mode (optional, mostly for unit tests) | ||
| 74 | + */ | ||
| 75 | + run: function(ex, mode) { | ||
| 76 | + ex = ex || this.createException(); | ||
| 77 | + // examine exception properties w/o debugger | ||
| 78 | + //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);} | ||
| 79 | + mode = mode || this.mode(ex); | ||
| 80 | + if (mode === 'other') { | ||
| 81 | + return this.other(arguments.callee); | ||
| 82 | + } else { | ||
| 83 | + return this[mode](ex); | ||
| 84 | + } | ||
| 85 | + }, | ||
| 86 | + | ||
| 87 | + createException: function() { | ||
| 88 | + try { | ||
| 89 | + this.undef(); | ||
| 90 | + } catch (e) { | ||
| 91 | + return e; | ||
| 92 | + } | ||
| 93 | + }, | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * Mode could differ for different exception, e.g. | ||
| 97 | + * exceptions in Chrome may or may not have arguments or stack. | ||
| 98 | + * | ||
| 99 | + * @return {String} mode of operation for the exception | ||
| 100 | + */ | ||
| 101 | + mode: function(e) { | ||
| 102 | + if (e['arguments'] && e.stack) { | ||
| 103 | + return 'chrome'; | ||
| 104 | + } else if (e.stack && e.sourceURL) { | ||
| 105 | + return 'safari'; | ||
| 106 | + } else if (e.stack && e.number) { | ||
| 107 | + return 'ie'; | ||
| 108 | + } else if (typeof e.message === 'string' && typeof window !== 'undefined' && window.opera) { | ||
| 109 | + // e.message.indexOf("Backtrace:") > -1 -> opera | ||
| 110 | + // !e.stacktrace -> opera | ||
| 111 | + if (!e.stacktrace) { | ||
| 112 | + return 'opera9'; // use e.message | ||
| 113 | + } | ||
| 114 | + // 'opera#sourceloc' in e -> opera9, opera10a | ||
| 115 | + if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) { | ||
| 116 | + return 'opera9'; // use e.message | ||
| 117 | + } | ||
| 118 | + // e.stacktrace && !e.stack -> opera10a | ||
| 119 | + if (!e.stack) { | ||
| 120 | + return 'opera10a'; // use e.stacktrace | ||
| 121 | + } | ||
| 122 | + // e.stacktrace && e.stack -> opera10b | ||
| 123 | + if (e.stacktrace.indexOf("called from line") < 0) { | ||
| 124 | + return 'opera10b'; // use e.stacktrace, format differs from 'opera10a' | ||
| 125 | + } | ||
| 126 | + // e.stacktrace && e.stack -> opera11 | ||
| 127 | + return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b' | ||
| 128 | + } else if (e.stack && !e.fileName) { | ||
| 129 | + // Chrome 27 does not have e.arguments as earlier versions, | ||
| 130 | + // but still does not have e.fileName as Firefox | ||
| 131 | + return 'chrome'; | ||
| 132 | + } else if (e.stack) { | ||
| 133 | + return 'firefox'; | ||
| 134 | + } | ||
| 135 | + return 'other'; | ||
| 136 | + }, | ||
| 137 | + | ||
| 138 | + /** | ||
| 139 | + * Given a context, function name, and callback function, overwrite it so that it calls | ||
| 140 | + * printStackTrace() first with a callback and then runs the rest of the body. | ||
| 141 | + * | ||
| 142 | + * @param {Object} context of execution (e.g. window) | ||
| 143 | + * @param {String} functionName to instrument | ||
| 144 | + * @param {Function} callback function to call with a stack trace on invocation | ||
| 145 | + */ | ||
| 146 | + instrumentFunction: function(context, functionName, callback) { | ||
| 147 | + context = context || window; | ||
| 148 | + var original = context[functionName]; | ||
| 149 | + context[functionName] = function instrumented() { | ||
| 150 | + callback.call(this, printStackTrace().slice(4)); | ||
| 151 | + return context[functionName]._instrumented.apply(this, arguments); | ||
| 152 | + }; | ||
| 153 | + context[functionName]._instrumented = original; | ||
| 154 | + }, | ||
| 155 | + | ||
| 156 | + /** | ||
| 157 | + * Given a context and function name of a function that has been | ||
| 158 | + * instrumented, revert the function to it's original (non-instrumented) | ||
| 159 | + * state. | ||
| 160 | + * | ||
| 161 | + * @param {Object} context of execution (e.g. window) | ||
| 162 | + * @param {String} functionName to de-instrument | ||
| 163 | + */ | ||
| 164 | + deinstrumentFunction: function(context, functionName) { | ||
| 165 | + if (context[functionName].constructor === Function && | ||
| 166 | + context[functionName]._instrumented && | ||
| 167 | + context[functionName]._instrumented.constructor === Function) { | ||
| 168 | + context[functionName] = context[functionName]._instrumented; | ||
| 169 | + } | ||
| 170 | + }, | ||
| 171 | + | ||
| 172 | + /** | ||
| 173 | + * Given an Error object, return a formatted Array based on Chrome's stack string. | ||
| 174 | + * | ||
| 175 | + * @param e - Error object to inspect | ||
| 176 | + * @return Array<String> of function calls, files and line numbers | ||
| 177 | + */ | ||
| 178 | + chrome: function(e) { | ||
| 179 | + var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, ''). | ||
| 180 | + replace(/^\s+(at eval )?at\s+/gm, ''). | ||
| 181 | + replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2'). | ||
| 182 | + replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n'); | ||
| 183 | + stack.pop(); | ||
| 184 | + return stack; | ||
| 185 | + }, | ||
| 186 | + | ||
| 187 | + /** | ||
| 188 | + * Given an Error object, return a formatted Array based on Safari's stack string. | ||
| 189 | + * | ||
| 190 | + * @param e - Error object to inspect | ||
| 191 | + * @return Array<String> of function calls, files and line numbers | ||
| 192 | + */ | ||
| 193 | + safari: function(e) { | ||
| 194 | + return e.stack.replace(/\[native code\]\n/m, '') | ||
| 195 | + .replace(/^(?=\w+Error\:).*$\n/m, '') | ||
| 196 | + .replace(/^@/gm, '{anonymous}()@') | ||
| 197 | + .split('\n'); | ||
| 198 | + }, | ||
| 199 | + | ||
| 200 | + /** | ||
| 201 | + * Given an Error object, return a formatted Array based on IE's stack string. | ||
| 202 | + * | ||
| 203 | + * @param e - Error object to inspect | ||
| 204 | + * @return Array<String> of function calls, files and line numbers | ||
| 205 | + */ | ||
| 206 | + ie: function(e) { | ||
| 207 | + var lineRE = /^.*at (\w+) \(([^\)]+)\)$/gm; | ||
| 208 | + return e.stack.replace(/at Anonymous function /gm, '{anonymous}()@') | ||
| 209 | + .replace(/^(?=\w+Error\:).*$\n/m, '') | ||
| 210 | + .replace(lineRE, '$1@$2') | ||
| 211 | + .split('\n'); | ||
| 212 | + }, | ||
| 213 | + | ||
| 214 | + /** | ||
| 215 | + * Given an Error object, return a formatted Array based on Firefox's stack string. | ||
| 216 | + * | ||
| 217 | + * @param e - Error object to inspect | ||
| 218 | + * @return Array<String> of function calls, files and line numbers | ||
| 219 | + */ | ||
| 220 | + firefox: function(e) { | ||
| 221 | + return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^[\(@]/gm, '{anonymous}()@').split('\n'); | ||
| 222 | + }, | ||
| 223 | + | ||
| 224 | + opera11: function(e) { | ||
| 225 | + var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/; | ||
| 226 | + var lines = e.stacktrace.split('\n'), result = []; | ||
| 227 | + | ||
| 228 | + for (var i = 0, len = lines.length; i < len; i += 2) { | ||
| 229 | + var match = lineRE.exec(lines[i]); | ||
| 230 | + if (match) { | ||
| 231 | + var location = match[4] + ':' + match[1] + ':' + match[2]; | ||
| 232 | + var fnName = match[3] || "global code"; | ||
| 233 | + fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON); | ||
| 234 | + result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, '')); | ||
| 235 | + } | ||
| 236 | + } | ||
| 237 | + | ||
| 238 | + return result; | ||
| 239 | + }, | ||
| 240 | + | ||
| 241 | + opera10b: function(e) { | ||
| 242 | + // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" + | ||
| 243 | + // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" + | ||
| 244 | + // "@file://localhost/G:/js/test/functional/testcase1.html:15" | ||
| 245 | + var lineRE = /^(.*)@(.+):(\d+)$/; | ||
| 246 | + var lines = e.stacktrace.split('\n'), result = []; | ||
| 247 | + | ||
| 248 | + for (var i = 0, len = lines.length; i < len; i++) { | ||
| 249 | + var match = lineRE.exec(lines[i]); | ||
| 250 | + if (match) { | ||
| 251 | + var fnName = match[1]? (match[1] + '()') : "global code"; | ||
| 252 | + result.push(fnName + '@' + match[2] + ':' + match[3]); | ||
| 253 | + } | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + return result; | ||
| 257 | + }, | ||
| 258 | + | ||
| 259 | + /** | ||
| 260 | + * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. | ||
| 261 | + * | ||
| 262 | + * @param e - Error object to inspect | ||
| 263 | + * @return Array<String> of function calls, files and line numbers | ||
| 264 | + */ | ||
| 265 | + opera10a: function(e) { | ||
| 266 | + // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n" | ||
| 267 | + // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n" | ||
| 268 | + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; | ||
| 269 | + var lines = e.stacktrace.split('\n'), result = []; | ||
| 270 | + | ||
| 271 | + for (var i = 0, len = lines.length; i < len; i += 2) { | ||
| 272 | + var match = lineRE.exec(lines[i]); | ||
| 273 | + if (match) { | ||
| 274 | + var fnName = match[3] || ANON; | ||
| 275 | + result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); | ||
| 276 | + } | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + return result; | ||
| 280 | + }, | ||
| 281 | + | ||
| 282 | + // Opera 7.x-9.2x only! | ||
| 283 | + opera9: function(e) { | ||
| 284 | + // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n" | ||
| 285 | + // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n" | ||
| 286 | + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i; | ||
| 287 | + var lines = e.message.split('\n'), result = []; | ||
| 288 | + | ||
| 289 | + for (var i = 2, len = lines.length; i < len; i += 2) { | ||
| 290 | + var match = lineRE.exec(lines[i]); | ||
| 291 | + if (match) { | ||
| 292 | + result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); | ||
| 293 | + } | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + return result; | ||
| 297 | + }, | ||
| 298 | + | ||
| 299 | + // Safari 5-, IE 9-, and others | ||
| 300 | + other: function(curr) { | ||
| 301 | + var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10; | ||
| 302 | + while (curr && curr['arguments'] && stack.length < maxStackSize) { | ||
| 303 | + fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; | ||
| 304 | + args = Array.prototype.slice.call(curr['arguments'] || []); | ||
| 305 | + stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; | ||
| 306 | + curr = curr.caller; | ||
| 307 | + } | ||
| 308 | + return stack; | ||
| 309 | + }, | ||
| 310 | + | ||
| 311 | + /** | ||
| 312 | + * Given arguments array as a String, substituting type names for non-string types. | ||
| 313 | + * | ||
| 314 | + * @param {Arguments,Array} args | ||
| 315 | + * @return {String} stringified arguments | ||
| 316 | + */ | ||
| 317 | + stringifyArguments: function(args) { | ||
| 318 | + var result = []; | ||
| 319 | + var slice = Array.prototype.slice; | ||
| 320 | + for (var i = 0; i < args.length; ++i) { | ||
| 321 | + var arg = args[i]; | ||
| 322 | + if (arg === undefined) { | ||
| 323 | + result[i] = 'undefined'; | ||
| 324 | + } else if (arg === null) { | ||
| 325 | + result[i] = 'null'; | ||
| 326 | + } else if (arg.constructor) { | ||
| 327 | + if (arg.constructor === Array) { | ||
| 328 | + if (arg.length < 3) { | ||
| 329 | + result[i] = '[' + this.stringifyArguments(arg) + ']'; | ||
| 330 | + } else { | ||
| 331 | + result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']'; | ||
| 332 | + } | ||
| 333 | + } else if (arg.constructor === Object) { | ||
| 334 | + result[i] = '#object'; | ||
| 335 | + } else if (arg.constructor === Function) { | ||
| 336 | + result[i] = '#function'; | ||
| 337 | + } else if (arg.constructor === String) { | ||
| 338 | + result[i] = '"' + arg + '"'; | ||
| 339 | + } else if (arg.constructor === Number) { | ||
| 340 | + result[i] = arg; | ||
| 341 | + } | ||
| 342 | + } | ||
| 343 | + } | ||
| 344 | + return result.join(','); | ||
| 345 | + }, | ||
| 346 | + | ||
| 347 | + sourceCache: {}, | ||
| 348 | + | ||
| 349 | + /** | ||
| 350 | + * @return the text from a given URL | ||
| 351 | + */ | ||
| 352 | + ajax: function(url) { | ||
| 353 | + var req = this.createXMLHTTPObject(); | ||
| 354 | + if (req) { | ||
| 355 | + try { | ||
| 356 | + req.open('GET', url, false); | ||
| 357 | + //req.overrideMimeType('text/plain'); | ||
| 358 | + //req.overrideMimeType('text/javascript'); | ||
| 359 | + req.send(null); | ||
| 360 | + //return req.status == 200 ? req.responseText : ''; | ||
| 361 | + return req.responseText; | ||
| 362 | + } catch (e) { | ||
| 363 | + } | ||
| 364 | + } | ||
| 365 | + return ''; | ||
| 366 | + }, | ||
| 367 | + | ||
| 368 | + /** | ||
| 369 | + * Try XHR methods in order and store XHR factory. | ||
| 370 | + * | ||
| 371 | + * @return <Function> XHR function or equivalent | ||
| 372 | + */ | ||
| 373 | + createXMLHTTPObject: function() { | ||
| 374 | + var xmlhttp, XMLHttpFactories = [ | ||
| 375 | + function() { | ||
| 376 | + return new XMLHttpRequest(); | ||
| 377 | + }, function() { | ||
| 378 | + return new ActiveXObject('Msxml2.XMLHTTP'); | ||
| 379 | + }, function() { | ||
| 380 | + return new ActiveXObject('Msxml3.XMLHTTP'); | ||
| 381 | + }, function() { | ||
| 382 | + return new ActiveXObject('Microsoft.XMLHTTP'); | ||
| 383 | + } | ||
| 384 | + ]; | ||
| 385 | + for (var i = 0; i < XMLHttpFactories.length; i++) { | ||
| 386 | + try { | ||
| 387 | + xmlhttp = XMLHttpFactories[i](); | ||
| 388 | + // Use memoization to cache the factory | ||
| 389 | + this.createXMLHTTPObject = XMLHttpFactories[i]; | ||
| 390 | + return xmlhttp; | ||
| 391 | + } catch (e) { | ||
| 392 | + } | ||
| 393 | + } | ||
| 394 | + }, | ||
| 395 | + | ||
| 396 | + /** | ||
| 397 | + * Given a URL, check if it is in the same domain (so we can get the source | ||
| 398 | + * via Ajax). | ||
| 399 | + * | ||
| 400 | + * @param url <String> source url | ||
| 401 | + * @return <Boolean> False if we need a cross-domain request | ||
| 402 | + */ | ||
| 403 | + isSameDomain: function(url) { | ||
| 404 | + return typeof location !== "undefined" && url.indexOf(location.hostname) !== -1; // location may not be defined, e.g. when running from nodejs. | ||
| 405 | + }, | ||
| 406 | + | ||
| 407 | + /** | ||
| 408 | + * Get source code from given URL if in the same domain. | ||
| 409 | + * | ||
| 410 | + * @param url <String> JS source URL | ||
| 411 | + * @return <Array> Array of source code lines | ||
| 412 | + */ | ||
| 413 | + getSource: function(url) { | ||
| 414 | + // TODO reuse source from script tags? | ||
| 415 | + if (!(url in this.sourceCache)) { | ||
| 416 | + this.sourceCache[url] = this.ajax(url).split('\n'); | ||
| 417 | + } | ||
| 418 | + return this.sourceCache[url]; | ||
| 419 | + }, | ||
| 420 | + | ||
| 421 | + guessAnonymousFunctions: function(stack) { | ||
| 422 | + for (var i = 0; i < stack.length; ++i) { | ||
| 423 | + var reStack = /\{anonymous\}\(.*\)@(.*)/, | ||
| 424 | + reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/, | ||
| 425 | + frame = stack[i], ref = reStack.exec(frame); | ||
| 426 | + | ||
| 427 | + if (ref) { | ||
| 428 | + var m = reRef.exec(ref[1]); | ||
| 429 | + if (m) { // If falsey, we did not get any file/line information | ||
| 430 | + var file = m[1], lineno = m[2], charno = m[3] || 0; | ||
| 431 | + if (file && this.isSameDomain(file) && lineno) { | ||
| 432 | + var functionName = this.guessAnonymousFunction(file, lineno, charno); | ||
| 433 | + stack[i] = frame.replace('{anonymous}', functionName); | ||
| 434 | + } | ||
| 435 | + } | ||
| 436 | + } | ||
| 437 | + } | ||
| 438 | + return stack; | ||
| 439 | + }, | ||
| 440 | + | ||
| 441 | + guessAnonymousFunction: function(url, lineNo, charNo) { | ||
| 442 | + var ret; | ||
| 443 | + try { | ||
| 444 | + ret = this.findFunctionName(this.getSource(url), lineNo); | ||
| 445 | + } catch (e) { | ||
| 446 | + ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString(); | ||
| 447 | + } | ||
| 448 | + return ret; | ||
| 449 | + }, | ||
| 450 | + | ||
| 451 | + findFunctionName: function(source, lineNo) { | ||
| 452 | + // FIXME findFunctionName fails for compressed source | ||
| 453 | + // (more than one function on the same line) | ||
| 454 | + // function {name}({args}) m[1]=name m[2]=args | ||
| 455 | + var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; | ||
| 456 | + // {name} = function ({args}) TODO args capture | ||
| 457 | + // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/ | ||
| 458 | + var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/; | ||
| 459 | + // {name} = eval() | ||
| 460 | + var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/; | ||
| 461 | + // Walk backwards in the source lines until we find | ||
| 462 | + // the line which matches one of the patterns above | ||
| 463 | + var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos; | ||
| 464 | + for (var i = 0; i < maxLines; ++i) { | ||
| 465 | + // lineNo is 1-based, source[] is 0-based | ||
| 466 | + line = source[lineNo - i - 1]; | ||
| 467 | + commentPos = line.indexOf('//'); | ||
| 468 | + if (commentPos >= 0) { | ||
| 469 | + line = line.substr(0, commentPos); | ||
| 470 | + } | ||
| 471 | + // TODO check other types of comments? Commented code may lead to false positive | ||
| 472 | + if (line) { | ||
| 473 | + code = line + code; | ||
| 474 | + m = reFunctionExpression.exec(code); | ||
| 475 | + if (m && m[1]) { | ||
| 476 | + return m[1]; | ||
| 477 | + } | ||
| 478 | + m = reFunctionDeclaration.exec(code); | ||
| 479 | + if (m && m[1]) { | ||
| 480 | + //return m[1] + "(" + (m[2] || "") + ")"; | ||
| 481 | + return m[1]; | ||
| 482 | + } | ||
| 483 | + m = reFunctionEvaluation.exec(code); | ||
| 484 | + if (m && m[1]) { | ||
| 485 | + return m[1]; | ||
| 486 | + } | ||
| 487 | + } | ||
| 488 | + } | ||
| 489 | + return '(?)'; | ||
| 490 | + } | ||
| 491 | +};// Airbrake JavaScript Notifier | ||
| 492 | +(function() { | ||
| 493 | + "use strict"; | ||
| 494 | + | ||
| 495 | + var NOTICE_XML = '<?xml version="1.0" encoding="UTF-8"?>' + | ||
| 496 | + '<notice version="2.0">' + | ||
| 497 | + '<api-key>{key}</api-key>' + | ||
| 498 | + '<notifier>' + | ||
| 499 | + '<name>airbrake_js</name>' + | ||
| 500 | + '<version>0.2.0</version>' + | ||
| 501 | + '<url>http://airbrake.io</url>' + | ||
| 502 | + '</notifier>' + | ||
| 503 | + '<error>' + | ||
| 504 | + '<class>{exception_class}</class>' + | ||
| 505 | + '<message><![CDATA[{exception_message}]]></message>' + | ||
| 506 | + '<backtrace>{backtrace_lines}</backtrace>' + | ||
| 507 | + '</error>' + | ||
| 508 | + '<request>' + | ||
| 509 | + '<url>{request_url}</url>' + | ||
| 510 | + '<component>{request_component}</component>' + | ||
| 511 | + '<action>{request_action}</action>' + | ||
| 512 | + '{request}' + | ||
| 513 | + '</request>' + | ||
| 514 | + '<server-environment>' + | ||
| 515 | + '<project-root>{project_root}</project-root>' + | ||
| 516 | + '<environment-name>{environment}</environment-name>' + | ||
| 517 | + '</server-environment>' + | ||
| 518 | + '<current-user>' + | ||
| 519 | + '<id>{user_id}</id>' + | ||
| 520 | + '<name>{user_name}</name>' + | ||
| 521 | + '<email>{user_email}</email>' + | ||
| 522 | + '</current-user>' + | ||
| 523 | + '</notice>', | ||
| 524 | + REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>', | ||
| 525 | + REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>', | ||
| 526 | + BACKTRACE_LINE_XML = '<line method="{function}" file="{file}" number="{line}" />', | ||
| 527 | + Config, | ||
| 528 | + Global, | ||
| 529 | + Util, | ||
| 530 | + _publicAPI, | ||
| 531 | + | ||
| 532 | + NOTICE_JSON = { | ||
| 533 | + "notifier": { | ||
| 534 | + "name": "airbrake_js", | ||
| 535 | + "version": "0.2.0", | ||
| 536 | + "url": "http://airbrake.io" | ||
| 537 | + }, | ||
| 538 | + "error": [ | ||
| 539 | + { | ||
| 540 | + "type": "{exception_class}", | ||
| 541 | + "message": "{exception_message}", | ||
| 542 | + "backtrace": [] | ||
| 543 | + | ||
| 544 | + } | ||
| 545 | + ], | ||
| 546 | + "context": { | ||
| 547 | + "language": "JavaScript", | ||
| 548 | + "environment": "{environment}", | ||
| 549 | + | ||
| 550 | + "version": "1.1.1", | ||
| 551 | + "url": "{request_url}", | ||
| 552 | + "rootDirectory": "{project_root}", | ||
| 553 | + "action": "{request_action}", | ||
| 554 | + | ||
| 555 | + "userId": "{user_id}", | ||
| 556 | + "userName": "{user_name}", | ||
| 557 | + "userEmail": "{user_email}", | ||
| 558 | + }, | ||
| 559 | + "environment": {}, | ||
| 560 | + //"session": "", | ||
| 561 | + "params": {}, | ||
| 562 | + }; | ||
| 563 | + | ||
| 564 | + Util = { | ||
| 565 | + /* | ||
| 566 | + * Merge a number of objects into one. | ||
| 567 | + * | ||
| 568 | + * Usage example: | ||
| 569 | + * var obj1 = { | ||
| 570 | + * a: 'a' | ||
| 571 | + * }, | ||
| 572 | + * obj2 = { | ||
| 573 | + * b: 'b' | ||
| 574 | + * }, | ||
| 575 | + * obj3 = { | ||
| 576 | + * c: 'c' | ||
| 577 | + * }, | ||
| 578 | + * mergedObj = Util.merge(obj1, obj2, obj3); | ||
| 579 | + * | ||
| 580 | + * mergedObj is: { | ||
| 581 | + * a: 'a', | ||
| 582 | + * b: 'b', | ||
| 583 | + * c: 'c' | ||
| 584 | + * } | ||
| 585 | + * | ||
| 586 | + */ | ||
| 587 | + merge: (function() { | ||
| 588 | + function processProperty (key, dest, src) { | ||
| 589 | + if (src.hasOwnProperty(key)) { | ||
| 590 | + dest[key] = src[key]; | ||
| 591 | + } | ||
| 592 | + } | ||
| 593 | + | ||
| 594 | + return function() { | ||
| 595 | + var objects = Array.prototype.slice.call(arguments), | ||
| 596 | + obj, | ||
| 597 | + key, | ||
| 598 | + result = {}; | ||
| 599 | + | ||
| 600 | + while (obj = objects.shift()) { | ||
| 601 | + for (key in obj) { | ||
| 602 | + processProperty(key, result, obj); | ||
| 603 | + } | ||
| 604 | + } | ||
| 605 | + | ||
| 606 | + return result; | ||
| 607 | + }; | ||
| 608 | + })(), | ||
| 609 | + | ||
| 610 | + /* | ||
| 611 | + * Replace &, <, >, ', " characters with correspondent HTML entities. | ||
| 612 | + */ | ||
| 613 | + escape: function (text) { | ||
| 614 | + return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | ||
| 615 | + .replace(/'/g, ''').replace(/"/g, '"'); | ||
| 616 | + }, | ||
| 617 | + | ||
| 618 | + /* | ||
| 619 | + * Remove leading and trailing space characters. | ||
| 620 | + */ | ||
| 621 | + trim: function (text) { | ||
| 622 | + return text.toString().replace(/^\s+/, '').replace(/\s+$/, ''); | ||
| 623 | + }, | ||
| 624 | + | ||
| 625 | + /* | ||
| 626 | + * Fill 'text' pattern with 'data' values. | ||
| 627 | + * | ||
| 628 | + * e.g. Utils.substitute('<{tag}></{tag}>', {tag: 'div'}, true) will return '<div></div>' | ||
| 629 | + * | ||
| 630 | + * emptyForUndefinedData - a flag, if true, all matched {<name>} without data.<name> value specified will be | ||
| 631 | + * replaced with empty string. | ||
| 632 | + */ | ||
| 633 | + substitute: function (text, data, emptyForUndefinedData) { | ||
| 634 | + return text.replace(/{([\w_.-]+)}/g, function(match, key) { | ||
| 635 | + return (key in data) ? data[key] : (emptyForUndefinedData ? '' : match); | ||
| 636 | + }); | ||
| 637 | + }, | ||
| 638 | + | ||
| 639 | + /* | ||
| 640 | + * Perform pattern rendering for an array of data objects. | ||
| 641 | + * Returns a concatenation of rendered strings of all objects in array. | ||
| 642 | + */ | ||
| 643 | + substituteArr: function (text, dataArr, emptyForUndefinedData) { | ||
| 644 | + var _i = 0, _l = 0, | ||
| 645 | + returnStr = ''; | ||
| 646 | + | ||
| 647 | + for (_i = 0, _l = dataArr.length; _i < _l; _i += 1) { | ||
| 648 | + returnStr += this.substitute(text, dataArr[_i], emptyForUndefinedData); | ||
| 649 | + } | ||
| 650 | + | ||
| 651 | + return returnStr; | ||
| 652 | + }, | ||
| 653 | + | ||
| 654 | + /* | ||
| 655 | + * Add hook for jQuery.fn.on function, to manualy call window.Airbrake.captureException() method | ||
| 656 | + * for every exception occurred. | ||
| 657 | + * | ||
| 658 | + * Let function 'f' be binded as an event handler: | ||
| 659 | + * | ||
| 660 | + * $(window).on 'click', f | ||
| 661 | + * | ||
| 662 | + * If an exception is occurred inside f's body, it will be catched here | ||
| 663 | + * and forwarded to captureException method. | ||
| 664 | + * | ||
| 665 | + * processjQueryEventHandlerWrapping is called every time window.Airbrake.setTrackJQ method is used, | ||
| 666 | + * if it switches previously setted value. | ||
| 667 | + */ | ||
| 668 | + processjQueryEventHandlerWrapping: function () { | ||
| 669 | + if (Config.options.trackJQ === true) { | ||
| 670 | + Config.jQuery_fn_on_original = Config.jQuery_fn_on_original || jQuery.fn.on; | ||
| 671 | + | ||
| 672 | + jQuery.fn.on = function () { | ||
| 673 | + var args = Array.prototype.slice.call(arguments), | ||
| 674 | + fnArgIdx = 4; | ||
| 675 | + | ||
| 676 | + // Search index of function argument | ||
| 677 | + while((--fnArgIdx > -1) && (typeof args[fnArgIdx] !== 'function')); | ||
| 678 | + | ||
| 679 | + // If the function is not found, then subscribe original event handler function | ||
| 680 | + if (fnArgIdx === -1) { | ||
| 681 | + return Config.jQuery_fn_on_original.apply(this, arguments); | ||
| 682 | + } | ||
| 683 | + | ||
| 684 | + // If the function is found, then subscribe wrapped event handler function | ||
| 685 | + args[fnArgIdx] = (function (fnOriginHandler) { | ||
| 686 | + return function() { | ||
| 687 | + try { | ||
| 688 | + fnOriginHandler.apply(this, arguments); | ||
| 689 | + } catch (e) { | ||
| 690 | + Global.captureException(e); | ||
| 691 | + } | ||
| 692 | + }; | ||
| 693 | + })(args[fnArgIdx]); | ||
| 694 | + | ||
| 695 | + // Call original jQuery.fn.on, with the same list of arguments, but | ||
| 696 | + // a function replaced with a proxy. | ||
| 697 | + return Config.jQuery_fn_on_original.apply(this, args); | ||
| 698 | + }; | ||
| 699 | + } else { | ||
| 700 | + // Recover original jQuery.fn.on if Config.options.trackJQ is set to false | ||
| 701 | + (typeof Config.jQuery_fn_on_original === 'function') && (jQuery.fn.on = Config.jQuery_fn_on_original); | ||
| 702 | + } | ||
| 703 | + }, | ||
| 704 | + | ||
| 705 | + isjQueryPresent: function () { | ||
| 706 | + // Currently only 1.7.x version supported | ||
| 707 | + return (typeof jQuery === 'function') && ('fn' in jQuery) && ('jquery' in jQuery.fn) | ||
| 708 | + && (jQuery.fn.jquery.indexOf('1.7') === 0) | ||
| 709 | + }, | ||
| 710 | + | ||
| 711 | + /* | ||
| 712 | + * Make first letter in a string capital. e.g. 'guessFunctionName' -> 'GuessFunctionName' | ||
| 713 | + * Is used to generate getter and setter method names. | ||
| 714 | + */ | ||
| 715 | + capitalizeFirstLetter: function (str) { | ||
| 716 | + return str.charAt(0).toUpperCase() + str.slice(1); | ||
| 717 | + }, | ||
| 718 | + | ||
| 719 | + /* | ||
| 720 | + * Generate public API from an array of specifically formated objects, e.g. | ||
| 721 | + * | ||
| 722 | + * - this will generate 'setEnvironment' and 'getEnvironment' API methods for configObj.xmlData.environment variable: | ||
| 723 | + * { | ||
| 724 | + * variable: 'environment', | ||
| 725 | + * namespace: 'xmlData' | ||
| 726 | + * } | ||
| 727 | + * | ||
| 728 | + * - this will define 'method' function as 'captureException' API method | ||
| 729 | + * { | ||
| 730 | + * methodName: 'captureException', | ||
| 731 | + * method: (function (...) {...}); | ||
| 732 | + * } | ||
| 733 | + * | ||
| 734 | + */ | ||
| 735 | + generatePublicAPI: (function () { | ||
| 736 | + function _generateSetter (variable, namespace, configObj) { | ||
| 737 | + return function (value) { | ||
| 738 | + configObj[namespace][variable] = value; | ||
| 739 | + }; | ||
| 740 | + } | ||
| 741 | + | ||
| 742 | + function _generateGetter (variable, namespace, configObj) { | ||
| 743 | + return function (value) { | ||
| 744 | + return configObj[namespace][variable]; | ||
| 745 | + }; | ||
| 746 | + } | ||
| 747 | + | ||
| 748 | + /* | ||
| 749 | + * publicAPI: array of specifically formated objects | ||
| 750 | + * configObj: inner configuration object | ||
| 751 | + */ | ||
| 752 | + return function (publicAPI, configObj) { | ||
| 753 | + var _i = 0, _m = null, _capitalized = '', | ||
| 754 | + returnObj = {}; | ||
| 755 | + | ||
| 756 | + for (_i = 0; _i < publicAPI.length; _i += 1) { | ||
| 757 | + _m = publicAPI[_i]; | ||
| 758 | + | ||
| 759 | + switch (true) { | ||
| 760 | + case (typeof _m.variable !== 'undefined') && (typeof _m.methodName === 'undefined'): | ||
| 761 | + _capitalized = Util.capitalizeFirstLetter(_m.variable) | ||
| 762 | + returnObj['set' + _capitalized] = _generateSetter(_m.variable, _m.namespace, configObj); | ||
| 763 | + returnObj['get' + _capitalized] = _generateGetter(_m.variable, _m.namespace, configObj); | ||
| 764 | + | ||
| 765 | + break; | ||
| 766 | + case (typeof _m.methodName !== 'undefined') && (typeof _m.method !== 'undefined'): | ||
| 767 | + returnObj[_m.methodName] = _m.method | ||
| 768 | + | ||
| 769 | + break; | ||
| 770 | + | ||
| 771 | + default: | ||
| 772 | + } | ||
| 773 | + } | ||
| 774 | + | ||
| 775 | + return returnObj; | ||
| 776 | + }; | ||
| 777 | + } ()) | ||
| 778 | + }; | ||
| 779 | + | ||
| 780 | + /* | ||
| 781 | + * The object to store settings. Allocated from the Global (windows scope) so that users can change settings | ||
| 782 | + * only through the methods, rather than through a direct change of the object fileds. So that we can to handle | ||
| 783 | + * change settings event (in setter method). | ||
| 784 | + */ | ||
| 785 | + Config = { | ||
| 786 | + xmlData: { | ||
| 787 | + environment: 'environment' | ||
| 788 | + }, | ||
| 789 | + | ||
| 790 | + options: { | ||
| 791 | + trackJQ: false, // jQuery.fn.jquery | ||
| 792 | + host: 'api.airbrake.io', | ||
| 793 | + errorDefaults: {}, | ||
| 794 | + guessFunctionName: false, | ||
| 795 | + requestType: 'GET', // Can be 'POST' or 'GET' | ||
| 796 | + outputFormat: 'XML' // Can be 'XML' or 'JSON' | ||
| 797 | + } | ||
| 798 | + }; | ||
| 799 | + | ||
| 800 | + /* | ||
| 801 | + * The public API definition object. If no 'methodName' and 'method' values specified, | ||
| 802 | + * getter and setter for 'variable' will be defined. | ||
| 803 | + */ | ||
| 804 | + _publicAPI = [ | ||
| 805 | + { | ||
| 806 | + variable: 'environment', | ||
| 807 | + namespace: 'xmlData' | ||
| 808 | + }, { | ||
| 809 | + variable: 'key', | ||
| 810 | + namespace: 'xmlData' | ||
| 811 | + }, { | ||
| 812 | + variable: 'host', | ||
| 813 | + namespace: 'options' | ||
| 814 | + },{ | ||
| 815 | + variable: 'projectId', | ||
| 816 | + namespace: 'options' | ||
| 817 | + },{ | ||
| 818 | + variable: 'errorDefaults', | ||
| 819 | + namespace: 'options' | ||
| 820 | + }, { | ||
| 821 | + variable: 'guessFunctionName', | ||
| 822 | + namespace: 'options' | ||
| 823 | + }, { | ||
| 824 | + variable: 'outputFormat', | ||
| 825 | + namespace: 'options' | ||
| 826 | + }, { | ||
| 827 | + methodName: 'setCurrentUser', | ||
| 828 | + method: (function (value) { | ||
| 829 | + for (var key in value) { | ||
| 830 | + if (value.hasOwnProperty(key)) { | ||
| 831 | + Config.xmlData['user_' + key] = value[key]; | ||
| 832 | + } | ||
| 833 | + } | ||
| 834 | + }) | ||
| 835 | + }, { | ||
| 836 | + methodName: 'setTrackJQ', | ||
| 837 | + variable: 'trackJQ', | ||
| 838 | + namespace: 'options', | ||
| 839 | + method: (function (value) { | ||
| 840 | + if (!Util.isjQueryPresent()) { | ||
| 841 | + throw Error('Please do not call \'Airbrake.setTrackJQ\' if jQuery does\'t present'); | ||
| 842 | + } | ||
| 843 | + | ||
| 844 | + value = !!value; | ||
| 845 | + | ||
| 846 | + if (Config.options.trackJQ === value) { | ||
| 847 | + return; | ||
| 848 | + } | ||
| 849 | + | ||
| 850 | + Config.options.trackJQ = value; | ||
| 851 | + | ||
| 852 | + Util.processjQueryEventHandlerWrapping(); | ||
| 853 | + }) | ||
| 854 | + }, { | ||
| 855 | + methodName: 'captureException', | ||
| 856 | + method: (function (e) { | ||
| 857 | + new Notifier().notify({ | ||
| 858 | + message: e.message, | ||
| 859 | + stack: e.stack | ||
| 860 | + }); | ||
| 861 | + }) | ||
| 862 | + } | ||
| 863 | + ]; | ||
| 864 | + | ||
| 865 | + // Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility) | ||
| 866 | + Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config); | ||
| 867 | + | ||
| 868 | + function Notifier() { | ||
| 869 | + this.options = Util.merge({}, Config.options); | ||
| 870 | + this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData); | ||
| 871 | + } | ||
| 872 | + | ||
| 873 | + Notifier.prototype = { | ||
| 874 | + constructor: Notifier, | ||
| 875 | + VERSION: '0.2.0', | ||
| 876 | + ROOT: window.location.protocol + '//' + window.location.host, | ||
| 877 | + BACKTRACE_MATCHER: /^(.*)\@(.*)\:(\d+)$/, | ||
| 878 | + backtrace_filters: [/notifier\.js/], | ||
| 879 | + DEF_XML_DATA: { | ||
| 880 | + request: {} | ||
| 881 | + }, | ||
| 882 | + | ||
| 883 | + notify: (function () { | ||
| 884 | + /* | ||
| 885 | + * Emit GET request via <iframe> element. | ||
| 886 | + * Data is transmited as a part of query string. | ||
| 887 | + */ | ||
| 888 | + function _sendGETRequest (url, data) { | ||
| 889 | + var request = document.createElement('iframe'); | ||
| 890 | + | ||
| 891 | + request.style.display = 'none'; | ||
| 892 | + request.src = url + '?data=' + data; | ||
| 893 | + | ||
| 894 | + // When request has been sent, delete iframe | ||
| 895 | + request.onload = function () { | ||
| 896 | + // To avoid infinite progress indicator | ||
| 897 | + setTimeout(function() { | ||
| 898 | + document.body.removeChild(request); | ||
| 899 | + }, 0); | ||
| 900 | + }; | ||
| 901 | + | ||
| 902 | + document.body.appendChild(request); | ||
| 903 | + } | ||
| 904 | + | ||
| 905 | + /* | ||
| 906 | + * Cross-domain AJAX POST request. | ||
| 907 | + * | ||
| 908 | + * It requires a server setup as described in Cross-Origin Resource Sharing spec: | ||
| 909 | + * http://www.w3.org/TR/cors/ | ||
| 910 | + */ | ||
| 911 | + function _sendPOSTRequest (url, data) { | ||
| 912 | + var request = new XMLHttpRequest(); | ||
| 913 | + request.open('POST', url, true); | ||
| 914 | + request.setRequestHeader('Content-Type', 'application/json'); | ||
| 915 | + request.send(data); | ||
| 916 | + } | ||
| 917 | + | ||
| 918 | + return function (error) { | ||
| 919 | + var outputData = '', | ||
| 920 | + url = ''; | ||
| 921 | + // | ||
| 922 | + | ||
| 923 | + /* | ||
| 924 | + * Should be changed to url = '//' + ... | ||
| 925 | + * to use the protocol of current page (http or https). Only sends 'secure' if page is secure. | ||
| 926 | + * XML uses V2 API. http://collect.airbrake.io/notifier_api/v2/notices | ||
| 927 | + */ | ||
| 928 | + | ||
| 929 | + | ||
| 930 | + switch (this.options['outputFormat']) { | ||
| 931 | + case 'XML': | ||
| 932 | + outputData = encodeURIComponent(this.generateXML(this.generateDataJSON(error))); | ||
| 933 | + url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/notifier_api/v2/notices'; | ||
| 934 | + _sendGETRequest(url, outputData); | ||
| 935 | + break; | ||
| 936 | + | ||
| 937 | + case 'JSON': | ||
| 938 | + /* | ||
| 939 | + * JSON uses API V3. Needs project in URL. | ||
| 940 | + * http://collect.airbrake.io/api/v3/projects/[PROJECT_ID]/notices?key=[API_KEY] | ||
| 941 | + * url = window.location.protocol + '://' + this.options.host + '/api/v3/projects' + this.options.projectId + '/notices?key=' + this.options.key; | ||
| 942 | + */ | ||
| 943 | + outputData = JSON.stringify(this.generateJSON(this.generateDataJSON(error))); | ||
| 944 | + url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/api/v3/projects/' + this.options.projectId + '/notices?key=' + this.xmlData.key; | ||
| 945 | + _sendPOSTRequest(url, outputData); | ||
| 946 | + break; | ||
| 947 | + | ||
| 948 | + default: | ||
| 949 | + } | ||
| 950 | + | ||
| 951 | + }; | ||
| 952 | + } ()), | ||
| 953 | + | ||
| 954 | + /* | ||
| 955 | + * Generate inner JSON representation of exception data that can be rendered as XML or JSON. | ||
| 956 | + */ | ||
| 957 | + generateDataJSON: (function () { | ||
| 958 | + /* | ||
| 959 | + * Generate variables array for inputObj object. | ||
| 960 | + * | ||
| 961 | + * e.g. | ||
| 962 | + * | ||
| 963 | + * _generateVariables({a: 'a'}) -> [{key: 'a', value: 'a'}] | ||
| 964 | + * | ||
| 965 | + */ | ||
| 966 | + function _generateVariables (inputObj) { | ||
| 967 | + var key = '', returnArr = []; | ||
| 968 | + | ||
| 969 | + for (key in inputObj) { | ||
| 970 | + if (inputObj.hasOwnProperty(key)) { | ||
| 971 | + returnArr.push({ | ||
| 972 | + key: key, | ||
| 973 | + value: inputObj[key] | ||
| 974 | + }); | ||
| 975 | + } | ||
| 976 | + } | ||
| 977 | + | ||
| 978 | + return returnArr; | ||
| 979 | + } | ||
| 980 | + | ||
| 981 | + /* | ||
| 982 | + * Generate Request part of notification. | ||
| 983 | + */ | ||
| 984 | + function _composeRequestObj (methods, errorObj) { | ||
| 985 | + var _i = 0, | ||
| 986 | + returnObj = {}, | ||
| 987 | + type = ''; | ||
| 988 | + | ||
| 989 | + for (_i = 0; _i < methods.length; _i += 1) { | ||
| 990 | + type = methods[_i]; | ||
| 991 | + if (typeof errorObj[type] !== 'undefined') { | ||
| 992 | + returnObj[type] = _generateVariables(errorObj[type]); | ||
| 993 | + } | ||
| 994 | + } | ||
| 995 | + | ||
| 996 | + return returnObj; | ||
| 997 | + } | ||
| 998 | + | ||
| 999 | + return function (errorWithoutDefaults) { | ||
| 1000 | + /* | ||
| 1001 | + * A constructor line: | ||
| 1002 | + * | ||
| 1003 | + * this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData); | ||
| 1004 | + */ | ||
| 1005 | + var outputData = this.xmlData, | ||
| 1006 | + error = Util.merge(this.options.errorDefaults, errorWithoutDefaults), | ||
| 1007 | + | ||
| 1008 | + component = error.component || '', | ||
| 1009 | + request_url = (error.url || '' + location.href), | ||
| 1010 | + | ||
| 1011 | + methods = ['cgi-data', 'params', 'session'], | ||
| 1012 | + _outputData = null; | ||
| 1013 | + | ||
| 1014 | + _outputData = { | ||
| 1015 | + request_url: request_url, | ||
| 1016 | + request_action: (error.action || ''), | ||
| 1017 | + request_component: component, | ||
| 1018 | + request: (function () { | ||
| 1019 | + if (request_url || component) { | ||
| 1020 | + error['cgi-data'] = error['cgi-data'] || {}; | ||
| 1021 | + error['cgi-data'].HTTP_USER_AGENT = navigator.userAgent; | ||
| 1022 | + return Util.merge(outputData.request, _composeRequestObj(methods, error)); | ||
| 1023 | + } else { | ||
| 1024 | + return {} | ||
| 1025 | + } | ||
| 1026 | + } ()), | ||
| 1027 | + | ||
| 1028 | + project_root: this.ROOT, | ||
| 1029 | + exception_class: (error.type || 'Error'), | ||
| 1030 | + exception_message: (error.message || 'Unknown error.'), | ||
| 1031 | + backtrace_lines: this.generateBacktrace(error) | ||
| 1032 | + } | ||
| 1033 | + | ||
| 1034 | + outputData = Util.merge(outputData, _outputData); | ||
| 1035 | + | ||
| 1036 | + return outputData; | ||
| 1037 | + }; | ||
| 1038 | + } ()), | ||
| 1039 | + | ||
| 1040 | + /* | ||
| 1041 | + * Generate XML notification from inner JSON representation. | ||
| 1042 | + * NOTICE_XML is used as pattern. | ||
| 1043 | + */ | ||
| 1044 | + generateXML: (function () { | ||
| 1045 | + function _generateRequestVariableGroups (requestObj) { | ||
| 1046 | + var _group = '', | ||
| 1047 | + returnStr = ''; | ||
| 1048 | + | ||
| 1049 | + for (_group in requestObj) { | ||
| 1050 | + if (requestObj.hasOwnProperty(_group)) { | ||
| 1051 | + returnStr += Util.substitute(REQUEST_VARIABLE_GROUP_XML, { | ||
| 1052 | + group_name: _group, | ||
| 1053 | + inner_content: Util.substituteArr(REQUEST_VARIABLE_XML, requestObj[_group], true) | ||
| 1054 | + }, true); | ||
| 1055 | + } | ||
| 1056 | + } | ||
| 1057 | + | ||
| 1058 | + return returnStr; | ||
| 1059 | + } | ||
| 1060 | + | ||
| 1061 | + return function (JSONdataObj) { | ||
| 1062 | + JSONdataObj.request = _generateRequestVariableGroups(JSONdataObj.request); | ||
| 1063 | + JSONdataObj.backtrace_lines = Util.substituteArr(BACKTRACE_LINE_XML, JSONdataObj.backtrace_lines, true); | ||
| 1064 | + | ||
| 1065 | + return Util.substitute(NOTICE_XML, JSONdataObj, true); | ||
| 1066 | + }; | ||
| 1067 | + } ()), | ||
| 1068 | + | ||
| 1069 | + /* | ||
| 1070 | + * Generate JSON notification from inner JSON representation. | ||
| 1071 | + * NOTICE_JSON is used as pattern. | ||
| 1072 | + */ | ||
| 1073 | + generateJSON: function (JSONdataObj) { | ||
| 1074 | + // Pattern string is JSON.stringify(NOTICE_JSON) | ||
| 1075 | + // The rendered string is parsed back as JSON. | ||
| 1076 | + var outputJSON = JSON.parse(Util.substitute(JSON.stringify(NOTICE_JSON), JSONdataObj, true)); | ||
| 1077 | + | ||
| 1078 | + // REMOVED - Request from JSON. | ||
| 1079 | + outputJSON.request = Util.merge(outputJSON.request, JSONdataObj.request); | ||
| 1080 | + outputJSON.error.backtrace = JSONdataObj.backtrace_lines; | ||
| 1081 | + | ||
| 1082 | + return outputJSON; | ||
| 1083 | + }, | ||
| 1084 | + | ||
| 1085 | + generateBacktrace: function (error) { | ||
| 1086 | + var backtrace = [], | ||
| 1087 | + file, | ||
| 1088 | + i, | ||
| 1089 | + matches, | ||
| 1090 | + stacktrace; | ||
| 1091 | + | ||
| 1092 | + error = error || {}; | ||
| 1093 | + | ||
| 1094 | + if (typeof error.stack !== 'string') { | ||
| 1095 | + try { | ||
| 1096 | + (0)(); | ||
| 1097 | + } catch (e) { | ||
| 1098 | + error.stack = e.stack; | ||
| 1099 | + } | ||
| 1100 | + } | ||
| 1101 | + | ||
| 1102 | + stacktrace = this.getStackTrace(error); | ||
| 1103 | + | ||
| 1104 | + for (i = 0; i < stacktrace.length; i++) { | ||
| 1105 | + matches = stacktrace[i].match(this.BACKTRACE_MATCHER); | ||
| 1106 | + | ||
| 1107 | + if (matches && this.validBacktraceLine(stacktrace[i])) { | ||
| 1108 | + file = matches[2].replace(this.ROOT, '[PROJECT_ROOT]'); | ||
| 1109 | + | ||
| 1110 | + if (i === 0 && matches[2].match(document.location.href)) { | ||
| 1111 | + // backtrace.push('<line method="" file="internal: " number=""/>'); | ||
| 1112 | + | ||
| 1113 | + backtrace.push({ | ||
| 1114 | + // Updated to fit in with V3 new terms for Backtrace data. | ||
| 1115 | + 'function': '', | ||
| 1116 | + file: 'internal: ', | ||
| 1117 | + line: '' | ||
| 1118 | + }); | ||
| 1119 | + } | ||
| 1120 | + | ||
| 1121 | + // backtrace.push('<line method="' + Util.escape(matches[1]) + '" file="' + Util.escape(file) + | ||
| 1122 | + // '" number="' + matches[3] + '" />'); | ||
| 1123 | + | ||
| 1124 | + backtrace.push({ | ||
| 1125 | + 'function': Util.escape(matches[1]), | ||
| 1126 | + file: Util.escape(file), | ||
| 1127 | + line: matches[3] | ||
| 1128 | + }); | ||
| 1129 | + } | ||
| 1130 | + } | ||
| 1131 | + | ||
| 1132 | + return backtrace; | ||
| 1133 | + }, | ||
| 1134 | + | ||
| 1135 | + getStackTrace: function (error) { | ||
| 1136 | + var i, | ||
| 1137 | + stacktrace = printStackTrace({ | ||
| 1138 | + e: error, | ||
| 1139 | + guess: this.options.guessFunctionName | ||
| 1140 | + }); | ||
| 1141 | + | ||
| 1142 | + for (i = 0; i < stacktrace.length; i++) { | ||
| 1143 | + if (stacktrace[i].match(/\:\d+$/)) { | ||
| 1144 | + continue; | ||
| 1145 | + } | ||
| 1146 | + | ||
| 1147 | + if (stacktrace[i].indexOf('@') === -1) { | ||
| 1148 | + stacktrace[i] += '@unsupported.js'; | ||
| 1149 | + } | ||
| 1150 | + | ||
| 1151 | + stacktrace[i] += ':0'; | ||
| 1152 | + } | ||
| 1153 | + | ||
| 1154 | + return stacktrace; | ||
| 1155 | + }, | ||
| 1156 | + | ||
| 1157 | + validBacktraceLine: function (line) { | ||
| 1158 | + for (var i = 0; i < this.backtrace_filters.length; i++) { | ||
| 1159 | + if (line.match(this.backtrace_filters[i])) { | ||
| 1160 | + return false; | ||
| 1161 | + } | ||
| 1162 | + } | ||
| 1163 | + | ||
| 1164 | + return true; | ||
| 1165 | + } | ||
| 1166 | + }; | ||
| 1167 | + | ||
| 1168 | + window.onerror = function (message, file, line) { | ||
| 1169 | + setTimeout(function () { | ||
| 1170 | + new Notifier().notify({ | ||
| 1171 | + message: message, | ||
| 1172 | + stack: '()@' + file + ':' + line | ||
| 1173 | + }); | ||
| 1174 | + }, 0); | ||
| 1175 | + | ||
| 1176 | + return true; | ||
| 1177 | + }; | ||
| 1178 | +})(); | ||
| 1179 | +})(window, document); |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +#!/usr/bin/env ruby | ||
| 2 | +require 'rubygems' | ||
| 3 | +require 'test_queue' | ||
| 4 | +require 'bundler' | ||
| 5 | +Bundler.setup(:default, :development, :test) | ||
| 6 | +require 'test_queue/runner/rspec' | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +class MongoidRspecRunner < TestQueue::Runner::RSpec | ||
| 10 | + def after_fork(num) | ||
| 11 | + super | ||
| 12 | + Mongoid.master = Mongoid.master.connection.db(Mongoid.master.name + "_#{num}") | ||
| 13 | + end | ||
| 14 | +end | ||
| 15 | + | ||
| 16 | +MongoidRspecRunner.new.execute |
spec/acceptance/sign_in_with_github_spec.rb
| @@ -11,7 +11,7 @@ feature 'Sign in with GitHub' do | @@ -11,7 +11,7 @@ feature 'Sign in with GitHub' do | ||
| 11 | 11 | ||
| 12 | visit '/' | 12 | visit '/' |
| 13 | click_link 'Sign in with GitHub' | 13 | click_link 'Sign in with GitHub' |
| 14 | - page.should have_content 'Successfully authorized from GitHub account' | 14 | + page.should have_content I18n.t("devise.omniauth_callbacks.success", :kind => 'GitHub') |
| 15 | end | 15 | end |
| 16 | 16 | ||
| 17 | scenario 'reject unrecognized user if authenticating via GitHub' do | 17 | scenario 'reject unrecognized user if authenticating via GitHub' do |
spec/controllers/api/v1/notices_controller_spec.rb
| @@ -9,25 +9,25 @@ describe Api::V1::NoticesController do | @@ -9,25 +9,25 @@ describe Api::V1::NoticesController do | ||
| 9 | 9 | ||
| 10 | describe "GET /api/v1/notices" do | 10 | describe "GET /api/v1/notices" do |
| 11 | before do | 11 | before do |
| 12 | - Fabricate(:notice, :created_at => DateTime.new(2012, 8, 01)) | ||
| 13 | - Fabricate(:notice, :created_at => DateTime.new(2012, 8, 01)) | ||
| 14 | - Fabricate(:notice, :created_at => DateTime.new(2012, 8, 21)) | ||
| 15 | - Fabricate(:notice, :created_at => DateTime.new(2012, 8, 30)) | 12 | + Fabricate(:notice, :created_at => Time.new(2012, 8, 01)) |
| 13 | + Fabricate(:notice, :created_at => Time.new(2012, 8, 01)) | ||
| 14 | + Fabricate(:notice, :created_at => Time.new(2012, 8, 21)) | ||
| 15 | + Fabricate(:notice, :created_at => Time.new(2012, 8, 30)) | ||
| 16 | end | 16 | end |
| 17 | 17 | ||
| 18 | it "should return JSON if JSON is requested" do | 18 | it "should return JSON if JSON is requested" do |
| 19 | get :index, :auth_token => @user.authentication_token, :format => "json" | 19 | get :index, :auth_token => @user.authentication_token, :format => "json" |
| 20 | - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError) | 20 | + expect { JSON.load(response.body) }.not_to raise_error() #JSON::ParserError) |
| 21 | end | 21 | end |
| 22 | 22 | ||
| 23 | it "should return XML if XML is requested" do | 23 | it "should return XML if XML is requested" do |
| 24 | get :index, :auth_token => @user.authentication_token, :format => "xml" | 24 | get :index, :auth_token => @user.authentication_token, :format => "xml" |
| 25 | - lambda { XML::Parser.string(response.body).parse }.should_not raise_error | 25 | + Nokogiri::XML(response.body).errors.should be_empty |
| 26 | end | 26 | end |
| 27 | 27 | ||
| 28 | it "should return JSON by default" do | 28 | it "should return JSON by default" do |
| 29 | get :index, :auth_token => @user.authentication_token | 29 | get :index, :auth_token => @user.authentication_token |
| 30 | - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError) | 30 | + expect { JSON.load(response.body) }.not_to raise_error() #JSON::ParserError) |
| 31 | end | 31 | end |
| 32 | 32 | ||
| 33 | describe "given a date range" do | 33 | describe "given a date range" do |
spec/controllers/api/v1/problems_controller_spec.rb
| @@ -19,17 +19,17 @@ describe Api::V1::ProblemsController do | @@ -19,17 +19,17 @@ describe Api::V1::ProblemsController do | ||
| 19 | 19 | ||
| 20 | it "should return JSON if JSON is requested" do | 20 | it "should return JSON if JSON is requested" do |
| 21 | get :index, :auth_token => @user.authentication_token, :format => "json" | 21 | get :index, :auth_token => @user.authentication_token, :format => "json" |
| 22 | - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError) | 22 | + expect { JSON.load(response.body) }.not_to raise_error()#JSON::ParserError) |
| 23 | end | 23 | end |
| 24 | 24 | ||
| 25 | it "should return XML if XML is requested" do | 25 | it "should return XML if XML is requested" do |
| 26 | get :index, :auth_token => @user.authentication_token, :format => "xml" | 26 | get :index, :auth_token => @user.authentication_token, :format => "xml" |
| 27 | - lambda { XML::Parser.string(response.body).parse }.should_not raise_error | 27 | + Nokogiri::XML(response.body).errors.should be_empty |
| 28 | end | 28 | end |
| 29 | 29 | ||
| 30 | it "should return JSON by default" do | 30 | it "should return JSON by default" do |
| 31 | get :index, :auth_token => @user.authentication_token | 31 | get :index, :auth_token => @user.authentication_token |
| 32 | - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError) | 32 | + expect { JSON.load(response.body) }.not_to raise_error()#JSON::ParserError) |
| 33 | end | 33 | end |
| 34 | 34 | ||
| 35 | 35 |
spec/controllers/apps_controller_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe AppsController do | 3 | describe AppsController do |
| 4 | - render_views | ||
| 5 | 4 | ||
| 6 | it_requires_authentication | 5 | it_requires_authentication |
| 7 | it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete} | 6 | it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete} |
| 8 | 7 | ||
| 9 | - | ||
| 10 | describe "GET /apps" do | 8 | describe "GET /apps" do |
| 11 | context 'when logged in as an admin' do | 9 | context 'when logged in as an admin' do |
| 12 | it 'finds all apps' do | 10 | it 'finds all apps' do |
| 13 | sign_in Fabricate(:admin) | 11 | sign_in Fabricate(:admin) |
| 14 | 3.times { Fabricate(:app) } | 12 | 3.times { Fabricate(:app) } |
| 15 | - apps = App.all | ||
| 16 | get :index | 13 | get :index |
| 17 | - assigns(:apps).should == apps | 14 | + controller.apps.entries.should == App.all.sort.entries |
| 18 | end | 15 | end |
| 19 | end | 16 | end |
| 20 | 17 | ||
| @@ -27,8 +24,8 @@ describe AppsController do | @@ -27,8 +24,8 @@ describe AppsController do | ||
| 27 | Fabricate(:user_watcher, :user => user, :app => watched_app1) | 24 | Fabricate(:user_watcher, :user => user, :app => watched_app1) |
| 28 | Fabricate(:user_watcher, :user => user, :app => watched_app2) | 25 | Fabricate(:user_watcher, :user => user, :app => watched_app2) |
| 29 | get :index | 26 | get :index |
| 30 | - assigns(:apps).should include(watched_app1, watched_app2) | ||
| 31 | - assigns(:apps).should_not include(unwatched_app) | 27 | + controller.apps.should include(watched_app1, watched_app2) |
| 28 | + controller.apps.should_not include(unwatched_app) | ||
| 32 | end | 29 | end |
| 33 | end | 30 | end |
| 34 | end | 31 | end |
| @@ -44,7 +41,7 @@ describe AppsController do | @@ -44,7 +41,7 @@ describe AppsController do | ||
| 44 | 41 | ||
| 45 | it 'finds the app' do | 42 | it 'finds the app' do |
| 46 | get :show, :id => @app.id | 43 | get :show, :id => @app.id |
| 47 | - assigns(:app).should == @app | 44 | + controller.app.should == @app |
| 48 | end | 45 | end |
| 49 | 46 | ||
| 50 | it "should not raise errors for app with err without notices" do | 47 | it "should not raise errors for app with err without notices" do |
| @@ -55,7 +52,6 @@ describe AppsController do | @@ -55,7 +52,6 @@ describe AppsController do | ||
| 55 | it "should list atom feed successfully" do | 52 | it "should list atom feed successfully" do |
| 56 | get :show, :id => @app.id, :format => "atom" | 53 | get :show, :id => @app.id, :format => "atom" |
| 57 | response.should be_success | 54 | response.should be_success |
| 58 | - response.body.should match(@problem.message) | ||
| 59 | end | 55 | end |
| 60 | 56 | ||
| 61 | context "pagination" do | 57 | context "pagination" do |
| @@ -65,13 +61,13 @@ describe AppsController do | @@ -65,13 +61,13 @@ describe AppsController do | ||
| 65 | 61 | ||
| 66 | it "should have default per_page value for user" do | 62 | it "should have default per_page value for user" do |
| 67 | get :show, :id => @app.id | 63 | get :show, :id => @app.id |
| 68 | - assigns(:problems).to_a.size.should == User::PER_PAGE | 64 | + controller.problems.to_a.size.should == User::PER_PAGE |
| 69 | end | 65 | end |
| 70 | 66 | ||
| 71 | it "should be able to override default per_page value" do | 67 | it "should be able to override default per_page value" do |
| 72 | @user.update_attribute :per_page, 10 | 68 | @user.update_attribute :per_page, 10 |
| 73 | get :show, :id => @app.id | 69 | get :show, :id => @app.id |
| 74 | - assigns(:problems).to_a.size.should == 10 | 70 | + controller.problems.to_a.size.should == 10 |
| 75 | end | 71 | end |
| 76 | end | 72 | end |
| 77 | 73 | ||
| @@ -85,14 +81,14 @@ describe AppsController do | @@ -85,14 +81,14 @@ describe AppsController do | ||
| 85 | context 'and no params' do | 81 | context 'and no params' do |
| 86 | it 'shows only unresolved problems' do | 82 | it 'shows only unresolved problems' do |
| 87 | get :show, :id => @app.id | 83 | get :show, :id => @app.id |
| 88 | - assigns(:problems).size.should == 1 | 84 | + controller.problems.size.should == 1 |
| 89 | end | 85 | end |
| 90 | end | 86 | end |
| 91 | 87 | ||
| 92 | context 'and all_problems=true params' do | 88 | context 'and all_problems=true params' do |
| 93 | it 'shows all errors' do | 89 | it 'shows all errors' do |
| 94 | get :show, :id => @app.id, :all_errs => true | 90 | get :show, :id => @app.id, :all_errs => true |
| 95 | - assigns(:problems).size.should == 2 | 91 | + controller.problems.size.should == 2 |
| 96 | end | 92 | end |
| 97 | end | 93 | end |
| 98 | end | 94 | end |
| @@ -108,35 +104,35 @@ describe AppsController do | @@ -108,35 +104,35 @@ describe AppsController do | ||
| 108 | context 'no params' do | 104 | context 'no params' do |
| 109 | it 'shows errs for all environments' do | 105 | it 'shows errs for all environments' do |
| 110 | get :show, :id => @app.id | 106 | get :show, :id => @app.id |
| 111 | - assigns(:problems).size.should == 21 | 107 | + controller.problems.size.should == 21 |
| 112 | end | 108 | end |
| 113 | end | 109 | end |
| 114 | 110 | ||
| 115 | context 'environment production' do | 111 | context 'environment production' do |
| 116 | it 'shows errs for just production' do | 112 | it 'shows errs for just production' do |
| 117 | get :show, :id => @app.id, :environment => 'production' | 113 | get :show, :id => @app.id, :environment => 'production' |
| 118 | - assigns(:problems).size.should == 6 | 114 | + controller.problems.size.should == 6 |
| 119 | end | 115 | end |
| 120 | end | 116 | end |
| 121 | 117 | ||
| 122 | context 'environment staging' do | 118 | context 'environment staging' do |
| 123 | it 'shows errs for just staging' do | 119 | it 'shows errs for just staging' do |
| 124 | get :show, :id => @app.id, :environment => 'staging' | 120 | get :show, :id => @app.id, :environment => 'staging' |
| 125 | - assigns(:problems).size.should == 5 | 121 | + controller.problems.size.should == 5 |
| 126 | end | 122 | end |
| 127 | end | 123 | end |
| 128 | 124 | ||
| 129 | context 'environment development' do | 125 | context 'environment development' do |
| 130 | it 'shows errs for just development' do | 126 | it 'shows errs for just development' do |
| 131 | get :show, :id => @app.id, :environment => 'development' | 127 | get :show, :id => @app.id, :environment => 'development' |
| 132 | - assigns(:problems).size.should == 5 | 128 | + controller.problems.size.should == 5 |
| 133 | end | 129 | end |
| 134 | end | 130 | end |
| 135 | 131 | ||
| 136 | context 'environment test' do | 132 | context 'environment test' do |
| 137 | it 'shows errs for just test' do | 133 | it 'shows errs for just test' do |
| 138 | get :show, :id => @app.id, :environment => 'test' | 134 | get :show, :id => @app.id, :environment => 'test' |
| 139 | - assigns(:problems).size.should == 5 | 135 | + controller.problems.size.should == 5 |
| 140 | end | 136 | end |
| 141 | end | 137 | end |
| 142 | end | 138 | end |
| @@ -149,7 +145,7 @@ describe AppsController do | @@ -149,7 +145,7 @@ describe AppsController do | ||
| 149 | watcher = Fabricate(:user_watcher, :app => app, :user => user) | 145 | watcher = Fabricate(:user_watcher, :app => app, :user => user) |
| 150 | sign_in user | 146 | sign_in user |
| 151 | get :show, :id => app.id | 147 | get :show, :id => app.id |
| 152 | - assigns(:app).should == app | 148 | + controller.app.should == app |
| 153 | end | 149 | end |
| 154 | 150 | ||
| 155 | it 'does not find the app if the user is not watching it' do | 151 | it 'does not find the app if the user is not watching it' do |
| @@ -170,19 +166,19 @@ describe AppsController do | @@ -170,19 +166,19 @@ describe AppsController do | ||
| 170 | describe "GET /apps/new" do | 166 | describe "GET /apps/new" do |
| 171 | it 'instantiates a new app with a prebuilt watcher' do | 167 | it 'instantiates a new app with a prebuilt watcher' do |
| 172 | get :new | 168 | get :new |
| 173 | - assigns(:app).should be_a(App) | ||
| 174 | - assigns(:app).should be_new_record | ||
| 175 | - assigns(:app).watchers.should_not be_empty | 169 | + controller.app.should be_a(App) |
| 170 | + controller.app.should be_new_record | ||
| 171 | + controller.app.watchers.should_not be_empty | ||
| 176 | end | 172 | end |
| 177 | 173 | ||
| 178 | it "should copy attributes from an existing app" do | 174 | it "should copy attributes from an existing app" do |
| 179 | @app = Fabricate(:app, :name => "do not copy", | 175 | @app = Fabricate(:app, :name => "do not copy", |
| 180 | :github_repo => "test/example") | 176 | :github_repo => "test/example") |
| 181 | get :new, :copy_attributes_from => @app.id | 177 | get :new, :copy_attributes_from => @app.id |
| 182 | - assigns(:app).should be_a(App) | ||
| 183 | - assigns(:app).should be_new_record | ||
| 184 | - assigns(:app).name.should be_blank | ||
| 185 | - assigns(:app).github_repo.should == "test/example" | 178 | + controller.app.should be_a(App) |
| 179 | + controller.app.should be_new_record | ||
| 180 | + controller.app.name.should be_blank | ||
| 181 | + controller.app.github_repo.should == "test/example" | ||
| 186 | end | 182 | end |
| 187 | end | 183 | end |
| 188 | 184 | ||
| @@ -190,7 +186,7 @@ describe AppsController do | @@ -190,7 +186,7 @@ describe AppsController do | ||
| 190 | it 'finds the correct app' do | 186 | it 'finds the correct app' do |
| 191 | app = Fabricate(:app) | 187 | app = Fabricate(:app) |
| 192 | get :edit, :id => app.id | 188 | get :edit, :id => app.id |
| 193 | - assigns(:app).should == app | 189 | + controller.app.should == app |
| 194 | end | 190 | end |
| 195 | end | 191 | end |
| 196 | 192 | ||
| @@ -316,7 +312,6 @@ describe AppsController do | @@ -316,7 +312,6 @@ describe AppsController do | ||
| 316 | 312 | ||
| 317 | @app.reload | 313 | @app.reload |
| 318 | @app.issue_tracker_configured?.should == false | 314 | @app.issue_tracker_configured?.should == false |
| 319 | - response.body.should match(/You must specify your/) | ||
| 320 | end | 315 | end |
| 321 | end | 316 | end |
| 322 | end | 317 | end |
| @@ -326,12 +321,11 @@ describe AppsController do | @@ -326,12 +321,11 @@ describe AppsController do | ||
| 326 | describe "DELETE /apps/:id" do | 321 | describe "DELETE /apps/:id" do |
| 327 | before do | 322 | before do |
| 328 | @app = Fabricate(:app) | 323 | @app = Fabricate(:app) |
| 329 | - App.stub(:find).with(@app.id).and_return(@app) | ||
| 330 | end | 324 | end |
| 331 | 325 | ||
| 332 | it "should find the app" do | 326 | it "should find the app" do |
| 333 | delete :destroy, :id => @app.id | 327 | delete :destroy, :id => @app.id |
| 334 | - assigns(:app).should == @app | 328 | + controller.app.should == @app |
| 335 | end | 329 | end |
| 336 | 330 | ||
| 337 | it "should destroy the app" do | 331 | it "should destroy the app" do |
spec/controllers/notices_controller_spec.rb
| @@ -3,57 +3,63 @@ require 'spec_helper' | @@ -3,57 +3,63 @@ require 'spec_helper' | ||
| 3 | describe NoticesController do | 3 | describe NoticesController do |
| 4 | it_requires_authentication :for => { :locate => :get } | 4 | it_requires_authentication :for => { :locate => :get } |
| 5 | 5 | ||
| 6 | + let(:notice) { Fabricate(:notice) } | ||
| 7 | + let(:xml) { Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read } | ||
| 6 | let(:app) { Fabricate(:app) } | 8 | let(:app) { Fabricate(:app) } |
| 9 | + let(:error_report) { double(:valid? => true, :generate_notice! => true, :notice => notice) } | ||
| 7 | 10 | ||
| 8 | context 'notices API' do | 11 | context 'notices API' do |
| 9 | - before do | ||
| 10 | - @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read | ||
| 11 | - @app = Fabricate(:app_with_watcher) | ||
| 12 | - App.stub(:find_by_api_key!).and_return(@app) | ||
| 13 | - @notice = App.report_error!(@xml) | ||
| 14 | - end | 12 | + context "with all params" do |
| 13 | + before do | ||
| 14 | + ErrorReport.should_receive(:new).with(xml).and_return(error_report) | ||
| 15 | + end | ||
| 15 | 16 | ||
| 16 | - it "generates a notice from raw xml [POST]" do | ||
| 17 | - App.should_receive(:report_error!).with(@xml).and_return(@notice) | ||
| 18 | - request.should_receive(:raw_post).and_return(@xml) | ||
| 19 | - post :create, :format => :xml | ||
| 20 | - response.should be_success | ||
| 21 | - # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53) | ||
| 22 | - # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb | ||
| 23 | - response.body.should match(%r{<id[^>]*>#{@notice.id}</id>}) | ||
| 24 | - response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>}) | ||
| 25 | - end | 17 | + context "with xml pass in raw_port" do |
| 18 | + before do | ||
| 19 | + request.should_receive(:raw_post).and_return(xml) | ||
| 20 | + post :create, :format => :xml | ||
| 21 | + end | ||
| 26 | 22 | ||
| 27 | - it "generates a notice from xml in a data param [POST]" do | ||
| 28 | - App.should_receive(:report_error!).with(@xml).and_return(@notice) | ||
| 29 | - post :create, :data => @xml, :format => :xml | ||
| 30 | - response.should be_success | ||
| 31 | - # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53) | ||
| 32 | - # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb | ||
| 33 | - response.body.should match(%r{<id[^>]*>#{@notice.id}</id>}) | ||
| 34 | - response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>}) | ||
| 35 | - end | 23 | + it "generates a notice from raw xml [POST]" do |
| 24 | + response.should be_success | ||
| 25 | + # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53) | ||
| 26 | + # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb | ||
| 27 | + response.body.should match(%r{<id[^>]*>#{notice.id}</id>}) | ||
| 28 | + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(notice.id)}</url>}) | ||
| 29 | + end | ||
| 30 | + | ||
| 31 | + end | ||
| 36 | 32 | ||
| 37 | - it "generates a notice from xml [GET]" do | ||
| 38 | - App.should_receive(:report_error!).with(@xml).and_return(@notice) | ||
| 39 | - get :create, :data => @xml, :format => :xml | ||
| 40 | - response.should be_success | ||
| 41 | - response.body.should match(%r{<id[^>]*>#{@notice.id}</id>}) | ||
| 42 | - response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>}) | 33 | + it "generates a notice from xml in a data param [POST]" do |
| 34 | + post :create, :data => xml, :format => :xml | ||
| 35 | + response.should be_success | ||
| 36 | + # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53) | ||
| 37 | + # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb | ||
| 38 | + response.body.should match(%r{<id[^>]*>#{notice.id}</id>}) | ||
| 39 | + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(notice.id)}</url>}) | ||
| 40 | + end | ||
| 41 | + | ||
| 42 | + it "generates a notice from xml [GET]" do | ||
| 43 | + get :create, :data => xml, :format => :xml | ||
| 44 | + response.should be_success | ||
| 45 | + response.body.should match(%r{<id[^>]*>#{notice.id}</id>}) | ||
| 46 | + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(notice.id)}</url>}) | ||
| 47 | + end | ||
| 48 | + context "with an invalid API_KEY" do | ||
| 49 | + let(:error_report) { double(:valid? => false) } | ||
| 50 | + it 'return 422' do | ||
| 51 | + post :create, :format => :xml, :data => xml | ||
| 52 | + expect(response.status).to eq 422 | ||
| 53 | + end | ||
| 54 | + end | ||
| 43 | end | 55 | end |
| 44 | 56 | ||
| 45 | - it "sends a notification email" do | ||
| 46 | - App.should_receive(:report_error!).with(@xml).and_return(@notice) | ||
| 47 | - request.should_receive(:raw_post).and_return(@xml) | ||
| 48 | - post :create, :format => :xml | ||
| 49 | - response.should be_success | ||
| 50 | - response.body.should match(%r{<id[^>]*>#{@notice.id}</id>}) | ||
| 51 | - response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>}) | ||
| 52 | - email = ActionMailer::Base.deliveries.last | ||
| 53 | - email.to.should include(@app.watchers.first.email) | ||
| 54 | - email.subject.should include(@notice.message) | ||
| 55 | - email.subject.should include("[#{@app.name}]") | ||
| 56 | - email.subject.should include("[#{@notice.environment_name}]") | 57 | + context "without params needed" do |
| 58 | + it 'return 400' do | ||
| 59 | + post :create, :format => :xml | ||
| 60 | + expect(response.status).to eq 400 | ||
| 61 | + expect(response.body).to eq 'Need a data params in GET or raw post data' | ||
| 62 | + end | ||
| 57 | end | 63 | end |
| 58 | end | 64 | end |
| 59 | 65 |
spec/controllers/problems_controller_spec.rb
| @@ -3,7 +3,7 @@ require 'spec_helper' | @@ -3,7 +3,7 @@ require 'spec_helper' | ||
| 3 | describe ProblemsController do | 3 | describe ProblemsController do |
| 4 | 4 | ||
| 5 | it_requires_authentication :for => { | 5 | it_requires_authentication :for => { |
| 6 | - :index => :get, :all => :get, :show => :get, :resolve => :put | 6 | + :index => :get, :show => :get, :resolve => :put, :search => :get |
| 7 | }, | 7 | }, |
| 8 | :params => {:app_id => 'dummyid', :id => 'dummyid'} | 8 | :params => {:app_id => 'dummyid', :id => 'dummyid'} |
| 9 | 9 | ||
| @@ -12,7 +12,7 @@ describe ProblemsController do | @@ -12,7 +12,7 @@ describe ProblemsController do | ||
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | describe "GET /problems" do | 14 | describe "GET /problems" do |
| 15 | - render_views | 15 | + #render_views |
| 16 | context 'when logged in as an admin' do | 16 | context 'when logged in as an admin' do |
| 17 | before(:each) do | 17 | before(:each) do |
| 18 | @user = Fabricate(:admin) | 18 | @user = Fabricate(:admin) |
| @@ -20,18 +20,6 @@ describe ProblemsController do | @@ -20,18 +20,6 @@ describe ProblemsController do | ||
| 20 | @problem = Fabricate(:notice, :err => Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production"))).problem | 20 | @problem = Fabricate(:notice, :err => Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production"))).problem |
| 21 | end | 21 | end |
| 22 | 22 | ||
| 23 | - it "should successfully list problems" do | ||
| 24 | - get :index | ||
| 25 | - response.should be_success | ||
| 26 | - response.body.gsub("​", "").should match(@problem.message) | ||
| 27 | - end | ||
| 28 | - | ||
| 29 | - it "should list atom feed successfully" do | ||
| 30 | - get :index, :format => "atom" | ||
| 31 | - response.should be_success | ||
| 32 | - response.body.should match(@problem.message) | ||
| 33 | - end | ||
| 34 | - | ||
| 35 | context "pagination" do | 23 | context "pagination" do |
| 36 | before(:each) do | 24 | before(:each) do |
| 37 | 35.times { Fabricate :err } | 25 | 35.times { Fabricate :err } |
| @@ -39,13 +27,13 @@ describe ProblemsController do | @@ -39,13 +27,13 @@ describe ProblemsController do | ||
| 39 | 27 | ||
| 40 | it "should have default per_page value for user" do | 28 | it "should have default per_page value for user" do |
| 41 | get :index | 29 | get :index |
| 42 | - assigns(:problems).to_a.size.should == User::PER_PAGE | 30 | + controller.problems.to_a.size.should == User::PER_PAGE |
| 43 | end | 31 | end |
| 44 | 32 | ||
| 45 | it "should be able to override default per_page value" do | 33 | it "should be able to override default per_page value" do |
| 46 | @user.update_attribute :per_page, 10 | 34 | @user.update_attribute :per_page, 10 |
| 47 | get :index | 35 | get :index |
| 48 | - assigns(:problems).to_a.size.should == 10 | 36 | + controller.problems.to_a.size.should == 10 |
| 49 | end | 37 | end |
| 50 | end | 38 | end |
| 51 | 39 | ||
| @@ -60,35 +48,35 @@ describe ProblemsController do | @@ -60,35 +48,35 @@ describe ProblemsController do | ||
| 60 | context 'no params' do | 48 | context 'no params' do |
| 61 | it 'shows problems for all environments' do | 49 | it 'shows problems for all environments' do |
| 62 | get :index | 50 | get :index |
| 63 | - assigns(:problems).size.should == 21 | 51 | + controller.problems.size.should == 21 |
| 64 | end | 52 | end |
| 65 | end | 53 | end |
| 66 | 54 | ||
| 67 | context 'environment production' do | 55 | context 'environment production' do |
| 68 | it 'shows problems for just production' do | 56 | it 'shows problems for just production' do |
| 69 | get :index, :environment => 'production' | 57 | get :index, :environment => 'production' |
| 70 | - assigns(:problems).size.should == 6 | 58 | + controller.problems.size.should == 6 |
| 71 | end | 59 | end |
| 72 | end | 60 | end |
| 73 | 61 | ||
| 74 | context 'environment staging' do | 62 | context 'environment staging' do |
| 75 | it 'shows problems for just staging' do | 63 | it 'shows problems for just staging' do |
| 76 | get :index, :environment => 'staging' | 64 | get :index, :environment => 'staging' |
| 77 | - assigns(:problems).size.should == 5 | 65 | + controller.problems.size.should == 5 |
| 78 | end | 66 | end |
| 79 | end | 67 | end |
| 80 | 68 | ||
| 81 | context 'environment development' do | 69 | context 'environment development' do |
| 82 | it 'shows problems for just development' do | 70 | it 'shows problems for just development' do |
| 83 | get :index, :environment => 'development' | 71 | get :index, :environment => 'development' |
| 84 | - assigns(:problems).size.should == 5 | 72 | + controller.problems.size.should == 5 |
| 85 | end | 73 | end |
| 86 | end | 74 | end |
| 87 | 75 | ||
| 88 | context 'environment test' do | 76 | context 'environment test' do |
| 89 | it 'shows problems for just test' do | 77 | it 'shows problems for just test' do |
| 90 | get :index, :environment => 'test' | 78 | get :index, :environment => 'test' |
| 91 | - assigns(:problems).size.should == 5 | 79 | + controller.problems.size.should == 5 |
| 92 | end | 80 | end |
| 93 | end | 81 | end |
| 94 | end | 82 | end |
| @@ -101,13 +89,13 @@ describe ProblemsController do | @@ -101,13 +89,13 @@ describe ProblemsController do | ||
| 101 | watched_unresolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false)) | 89 | watched_unresolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false)) |
| 102 | watched_resolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true)) | 90 | watched_resolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true)) |
| 103 | get :index | 91 | get :index |
| 104 | - assigns(:problems).should include(watched_unresolved_err.problem) | ||
| 105 | - assigns(:problems).should_not include(unwatched_err.problem, watched_resolved_err.problem) | 92 | + controller.problems.should include(watched_unresolved_err.problem) |
| 93 | + controller.problems.should_not include(unwatched_err.problem, watched_resolved_err.problem) | ||
| 106 | end | 94 | end |
| 107 | end | 95 | end |
| 108 | end | 96 | end |
| 109 | 97 | ||
| 110 | - describe "GET /problems/all" do | 98 | + describe "GET /problems - previously all" do |
| 111 | context 'when logged in as an admin' do | 99 | context 'when logged in as an admin' do |
| 112 | it "gets a paginated list of all problems" do | 100 | it "gets a paginated list of all problems" do |
| 113 | sign_in Fabricate(:admin) | 101 | sign_in Fabricate(:admin) |
| @@ -115,10 +103,10 @@ describe ProblemsController do | @@ -115,10 +103,10 @@ describe ProblemsController do | ||
| 115 | 3.times { problems << Fabricate(:err).problem } | 103 | 3.times { problems << Fabricate(:err).problem } |
| 116 | 3.times { problems << Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem } | 104 | 3.times { problems << Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem } |
| 117 | Problem.should_receive(:ordered_by).and_return( | 105 | Problem.should_receive(:ordered_by).and_return( |
| 118 | - mock('proxy', :page => mock('other_proxy', :per => problems)) | 106 | + double('proxy', :page => double('other_proxy', :per => problems)) |
| 119 | ) | 107 | ) |
| 120 | - get :all | ||
| 121 | - assigns(:problems).should == problems | 108 | + get :index, :all_errs => true |
| 109 | + controller.problems.should == problems | ||
| 122 | end | 110 | end |
| 123 | end | 111 | end |
| 124 | 112 | ||
| @@ -128,15 +116,15 @@ describe ProblemsController do | @@ -128,15 +116,15 @@ describe ProblemsController do | ||
| 128 | unwatched_problem = Fabricate(:problem) | 116 | unwatched_problem = Fabricate(:problem) |
| 129 | watched_unresolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false) | 117 | watched_unresolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false) |
| 130 | watched_resolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true) | 118 | watched_resolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true) |
| 131 | - get :all | ||
| 132 | - assigns(:problems).should include(watched_resolved_problem, watched_unresolved_problem) | ||
| 133 | - assigns(:problems).should_not include(unwatched_problem) | 119 | + get :index, :all_errs => true |
| 120 | + controller.problems.should include(watched_resolved_problem, watched_unresolved_problem) | ||
| 121 | + controller.problems.should_not include(unwatched_problem) | ||
| 134 | end | 122 | end |
| 135 | end | 123 | end |
| 136 | end | 124 | end |
| 137 | 125 | ||
| 138 | describe "GET /apps/:app_id/problems/:id" do | 126 | describe "GET /apps/:app_id/problems/:id" do |
| 139 | - render_views | 127 | + #render_views |
| 140 | 128 | ||
| 141 | context 'when logged in as an admin' do | 129 | context 'when logged in as an admin' do |
| 142 | before do | 130 | before do |
| @@ -145,12 +133,12 @@ describe ProblemsController do | @@ -145,12 +133,12 @@ describe ProblemsController do | ||
| 145 | 133 | ||
| 146 | it "finds the app" do | 134 | it "finds the app" do |
| 147 | get :show, :app_id => app.id, :id => err.problem.id | 135 | get :show, :app_id => app.id, :id => err.problem.id |
| 148 | - assigns(:app).should == app | 136 | + controller.app.should == app |
| 149 | end | 137 | end |
| 150 | 138 | ||
| 151 | it "finds the problem" do | 139 | it "finds the problem" do |
| 152 | get :show, :app_id => app.id, :id => err.problem.id | 140 | get :show, :app_id => app.id, :id => err.problem.id |
| 153 | - assigns(:problem).should == err.problem | 141 | + controller.problem.should == err.problem |
| 154 | end | 142 | end |
| 155 | 143 | ||
| 156 | it "successfully render page" do | 144 | it "successfully render page" do |
| @@ -178,32 +166,6 @@ describe ProblemsController do | @@ -178,32 +166,6 @@ describe ProblemsController do | ||
| 178 | end | 166 | end |
| 179 | end | 167 | end |
| 180 | 168 | ||
| 181 | - context "create issue button" do | ||
| 182 | - let(:button_matcher) { match(/create issue/) } | ||
| 183 | - | ||
| 184 | - it "should not exist for problem's app without issue tracker" do | ||
| 185 | - err = Fabricate :err | ||
| 186 | - get :show, :app_id => err.app.id, :id => err.problem.id | ||
| 187 | - | ||
| 188 | - response.body.should_not button_matcher | ||
| 189 | - end | ||
| 190 | - | ||
| 191 | - it "should exist for problem's app with issue tracker" do | ||
| 192 | - tracker = Fabricate(:lighthouse_tracker) | ||
| 193 | - err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app)) | ||
| 194 | - get :show, :app_id => err.app.id, :id => err.problem.id | ||
| 195 | - | ||
| 196 | - response.body.should button_matcher | ||
| 197 | - end | ||
| 198 | - | ||
| 199 | - it "should not exist for problem with issue_link" do | ||
| 200 | - tracker = Fabricate(:lighthouse_tracker) | ||
| 201 | - err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app, :issue_link => "http://some.host")) | ||
| 202 | - get :show, :app_id => err.app.id, :id => err.problem.id | ||
| 203 | - | ||
| 204 | - response.body.should_not button_matcher | ||
| 205 | - end | ||
| 206 | - end | ||
| 207 | end | 169 | end |
| 208 | 170 | ||
| 209 | context 'when logged in as a user' do | 171 | context 'when logged in as a user' do |
| @@ -217,7 +179,7 @@ describe ProblemsController do | @@ -217,7 +179,7 @@ describe ProblemsController do | ||
| 217 | 179 | ||
| 218 | it 'finds the problem if the user is watching the app' do | 180 | it 'finds the problem if the user is watching the app' do |
| 219 | get :show, :app_id => @watched_app.to_param, :id => @watched_err.problem.id | 181 | get :show, :app_id => @watched_app.to_param, :id => @watched_err.problem.id |
| 220 | - assigns(:problem).should == @watched_err.problem | 182 | + controller.problem.should == @watched_err.problem |
| 221 | end | 183 | end |
| 222 | 184 | ||
| 223 | it 'raises a DocumentNotFound error if the user is not watching the app' do | 185 | it 'raises a DocumentNotFound error if the user is not watching the app' do |
| @@ -233,17 +195,17 @@ describe ProblemsController do | @@ -233,17 +195,17 @@ describe ProblemsController do | ||
| 233 | sign_in Fabricate(:admin) | 195 | sign_in Fabricate(:admin) |
| 234 | 196 | ||
| 235 | @problem = Fabricate(:err) | 197 | @problem = Fabricate(:err) |
| 236 | - App.stub(:find).with(@problem.app.id).and_return(@problem.app) | 198 | + App.stub(:find).with(@problem.app.id.to_s).and_return(@problem.app) |
| 237 | @problem.app.problems.stub(:find).and_return(@problem.problem) | 199 | @problem.app.problems.stub(:find).and_return(@problem.problem) |
| 238 | @problem.problem.stub(:resolve!) | 200 | @problem.problem.stub(:resolve!) |
| 239 | end | 201 | end |
| 240 | 202 | ||
| 241 | it 'finds the app and the problem' do | 203 | it 'finds the app and the problem' do |
| 242 | - App.should_receive(:find).with(@problem.app.id).and_return(@problem.app) | 204 | + App.should_receive(:find).with(@problem.app.id.to_s).and_return(@problem.app) |
| 243 | @problem.app.problems.should_receive(:find).and_return(@problem.problem) | 205 | @problem.app.problems.should_receive(:find).and_return(@problem.problem) |
| 244 | put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id | 206 | put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id |
| 245 | - assigns(:app).should == @problem.app | ||
| 246 | - assigns(:problem).should == @problem.problem | 207 | + controller.app.should == @problem.app |
| 208 | + controller.problem.should == @problem.problem | ||
| 247 | end | 209 | end |
| 248 | 210 | ||
| 249 | it "should resolve the issue" do | 211 | it "should resolve the issue" do |
| @@ -269,7 +231,7 @@ describe ProblemsController do | @@ -269,7 +231,7 @@ describe ProblemsController do | ||
| 269 | end | 231 | end |
| 270 | 232 | ||
| 271 | describe "POST /apps/:app_id/problems/:id/create_issue" do | 233 | describe "POST /apps/:app_id/problems/:id/create_issue" do |
| 272 | - render_views | 234 | + #render_views |
| 273 | 235 | ||
| 274 | before(:each) do | 236 | before(:each) do |
| 275 | sign_in Fabricate(:admin) | 237 | sign_in Fabricate(:admin) |
| @@ -379,31 +341,25 @@ describe ProblemsController do | @@ -379,31 +341,25 @@ describe ProblemsController do | ||
| 379 | @problem2 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => false)).problem | 341 | @problem2 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => false)).problem |
| 380 | end | 342 | end |
| 381 | 343 | ||
| 382 | - it "should apply to multiple problems" do | ||
| 383 | - post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s] | ||
| 384 | - assigns(:selected_problems).should == [@problem1, @problem2] | ||
| 385 | - end | ||
| 386 | - | ||
| 387 | - it "should require at least one problem" do | ||
| 388 | - post :resolve_several, :problems => [] | ||
| 389 | - request.flash[:notice].should match(/You have not selected any/) | ||
| 390 | - end | ||
| 391 | - | ||
| 392 | context "POST /problems/merge_several" do | 344 | context "POST /problems/merge_several" do |
| 393 | it "should require at least two problems" do | 345 | it "should require at least two problems" do |
| 394 | post :merge_several, :problems => [@problem1.id.to_s] | 346 | post :merge_several, :problems => [@problem1.id.to_s] |
| 395 | - request.flash[:notice].should match(/You must select at least two/) | 347 | + request.flash[:notice].should eql I18n.t('controllers.problems.flash.need_two_errors_merge') |
| 396 | end | 348 | end |
| 397 | 349 | ||
| 398 | it "should merge the problems" do | 350 | it "should merge the problems" do |
| 399 | - lambda { | ||
| 400 | - post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s] | ||
| 401 | - assigns(:merged_problem).reload.errs.length.should == 2 | ||
| 402 | - }.should change(Problem, :count).by(-1) | 351 | + ProblemMerge.should_receive(:new).and_return(double(:merge => true)) |
| 352 | + post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s] | ||
| 403 | end | 353 | end |
| 404 | end | 354 | end |
| 405 | 355 | ||
| 406 | context "POST /problems/unmerge_several" do | 356 | context "POST /problems/unmerge_several" do |
| 357 | + | ||
| 358 | + it "should require at least one problem" do | ||
| 359 | + post :unmerge_several, :problems => [] | ||
| 360 | + request.flash[:notice].should eql I18n.t('controllers.problems.flash.no_select_problem') | ||
| 361 | + end | ||
| 362 | + | ||
| 407 | it "should unmerge a merged problem" do | 363 | it "should unmerge a merged problem" do |
| 408 | merged_problem = Problem.merge!(@problem1, @problem2) | 364 | merged_problem = Problem.merge!(@problem1, @problem2) |
| 409 | merged_problem.errs.length.should == 2 | 365 | merged_problem.errs.length.should == 2 |
| @@ -412,9 +368,16 @@ describe ProblemsController do | @@ -412,9 +368,16 @@ describe ProblemsController do | ||
| 412 | merged_problem.reload.errs.length.should == 1 | 368 | merged_problem.reload.errs.length.should == 1 |
| 413 | }.should change(Problem, :count).by(1) | 369 | }.should change(Problem, :count).by(1) |
| 414 | end | 370 | end |
| 371 | + | ||
| 415 | end | 372 | end |
| 416 | 373 | ||
| 417 | context "POST /problems/resolve_several" do | 374 | context "POST /problems/resolve_several" do |
| 375 | + | ||
| 376 | + it "should require at least one problem" do | ||
| 377 | + post :resolve_several, :problems => [] | ||
| 378 | + request.flash[:notice].should eql I18n.t('controllers.problems.flash.no_select_problem') | ||
| 379 | + end | ||
| 380 | + | ||
| 418 | it "should resolve the issue" do | 381 | it "should resolve the issue" do |
| 419 | post :resolve_several, :problems => [@problem2.id.to_s] | 382 | post :resolve_several, :problems => [@problem2.id.to_s] |
| 420 | @problem2.reload.resolved?.should == true | 383 | @problem2.reload.resolved?.should == true |
| @@ -428,10 +391,17 @@ describe ProblemsController do | @@ -428,10 +391,17 @@ describe ProblemsController do | ||
| 428 | it "should display a message about 2 errs" do | 391 | it "should display a message about 2 errs" do |
| 429 | post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s] | 392 | post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s] |
| 430 | flash[:success].should match(/2 errs have been resolved/) | 393 | flash[:success].should match(/2 errs have been resolved/) |
| 394 | + controller.selected_problems.should == [@problem1, @problem2] | ||
| 431 | end | 395 | end |
| 432 | end | 396 | end |
| 433 | 397 | ||
| 434 | context "POST /problems/unresolve_several" do | 398 | context "POST /problems/unresolve_several" do |
| 399 | + | ||
| 400 | + it "should require at least one problem" do | ||
| 401 | + post :unresolve_several, :problems => [] | ||
| 402 | + request.flash[:notice].should eql I18n.t('controllers.problems.flash.no_select_problem') | ||
| 403 | + end | ||
| 404 | + | ||
| 435 | it "should unresolve the issue" do | 405 | it "should unresolve the issue" do |
| 436 | post :unresolve_several, :problems => [@problem1.id.to_s] | 406 | post :unresolve_several, :problems => [@problem1.id.to_s] |
| 437 | @problem1.reload.resolved?.should == false | 407 | @problem1.reload.resolved?.should == false |
spec/controllers/users/omniauth_callbacks_controller_spec.rb
| @@ -12,7 +12,7 @@ describe Users::OmniauthCallbacksController do | @@ -12,7 +12,7 @@ describe Users::OmniauthCallbacksController do | ||
| 12 | :credentials => { :token => token } | 12 | :credentials => { :token => token } |
| 13 | ) | 13 | ) |
| 14 | } | 14 | } |
| 15 | - @controller.stub!(:env).and_return(env) | 15 | + @controller.stub(:env).and_return(env) |
| 16 | end | 16 | end |
| 17 | 17 | ||
| 18 | context 'Linking a GitHub account to a signed in user' do | 18 | context 'Linking a GitHub account to a signed in user' do |
spec/controllers/users_controller_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe UsersController do | 3 | describe UsersController do |
| 4 | - render_views | ||
| 5 | 4 | ||
| 6 | it_requires_authentication | 5 | it_requires_authentication |
| 7 | it_requires_admin_privileges :for => { | 6 | it_requires_admin_privileges :for => { |
| @@ -12,42 +11,39 @@ describe UsersController do | @@ -12,42 +11,39 @@ describe UsersController do | ||
| 12 | :destroy => :delete | 11 | :destroy => :delete |
| 13 | } | 12 | } |
| 14 | 13 | ||
| 14 | + let(:admin) { Fabricate(:admin) } | ||
| 15 | + let(:user) { Fabricate(:user) } | ||
| 16 | + let(:other_user) { Fabricate(:user) } | ||
| 17 | + | ||
| 15 | context 'Signed in as a regular user' do | 18 | context 'Signed in as a regular user' do |
| 19 | + | ||
| 16 | before do | 20 | before do |
| 17 | - sign_in @user = Fabricate(:user) | 21 | + sign_in user |
| 18 | end | 22 | end |
| 19 | 23 | ||
| 20 | it "should set a time zone" do | 24 | it "should set a time zone" do |
| 21 | - Time.zone.should.to_s == @user.time_zone | 25 | + Time.zone.should.to_s == user.time_zone |
| 22 | end | 26 | end |
| 23 | 27 | ||
| 24 | context "GET /users/:other_id/edit" do | 28 | context "GET /users/:other_id/edit" do |
| 25 | it "redirects to the home page" do | 29 | it "redirects to the home page" do |
| 26 | - get :edit, :id => Fabricate(:user).id | 30 | + get :edit, :id => other_user.id |
| 27 | response.should redirect_to(root_path) | 31 | response.should redirect_to(root_path) |
| 28 | end | 32 | end |
| 29 | end | 33 | end |
| 30 | 34 | ||
| 31 | context "GET /users/:my_id/edit" do | 35 | context "GET /users/:my_id/edit" do |
| 32 | it 'finds the user' do | 36 | it 'finds the user' do |
| 33 | - get :edit, :id => @user.id | ||
| 34 | - assigns(:user).should == @user | ||
| 35 | - end | ||
| 36 | - | ||
| 37 | - it "should have per_page option" do | ||
| 38 | - get :edit, :id => @user.id | ||
| 39 | - response.body.should match(/id="user_per_page"/) | 37 | + get :edit, :id => user.id |
| 38 | + controller.user.should == user | ||
| 39 | + expect(response).to render_template 'edit' | ||
| 40 | end | 40 | end |
| 41 | 41 | ||
| 42 | - it "should have time_zone option" do | ||
| 43 | - get :edit, :id => @user.id | ||
| 44 | - response.body.should match(/id="user_time_zone"/) | ||
| 45 | - end | ||
| 46 | end | 42 | end |
| 47 | 43 | ||
| 48 | context "PUT /users/:other_id" do | 44 | context "PUT /users/:other_id" do |
| 49 | it "redirects to the home page" do | 45 | it "redirects to the home page" do |
| 50 | - put :update, :id => Fabricate(:user).id | 46 | + put :update, :id => other_user.id |
| 51 | response.should redirect_to(root_path) | 47 | response.should redirect_to(root_path) |
| 52 | end | 48 | end |
| 53 | end | 49 | end |
| @@ -55,39 +51,47 @@ describe UsersController do | @@ -55,39 +51,47 @@ describe UsersController do | ||
| 55 | context "PUT /users/:my_id/id" do | 51 | context "PUT /users/:my_id/id" do |
| 56 | context "when the update is successful" do | 52 | context "when the update is successful" do |
| 57 | it "sets a message to display" do | 53 | it "sets a message to display" do |
| 58 | - put :update, :id => @user.to_param, :user => {:name => 'Kermit'} | 54 | + put :update, :id => user.to_param, :user => {:name => 'Kermit'} |
| 59 | request.flash[:success].should include('updated') | 55 | request.flash[:success].should include('updated') |
| 60 | end | 56 | end |
| 61 | 57 | ||
| 62 | it "redirects to the user's page" do | 58 | it "redirects to the user's page" do |
| 63 | - put :update, :id => @user.to_param, :user => {:name => 'Kermit'} | ||
| 64 | - response.should redirect_to(user_path(@user)) | 59 | + put :update, :id => user.to_param, :user => {:name => 'Kermit'} |
| 60 | + response.should redirect_to(user_path(user)) | ||
| 65 | end | 61 | end |
| 66 | 62 | ||
| 67 | it "should not be able to become an admin" do | 63 | it "should not be able to become an admin" do |
| 68 | - put :update, :id => @user.to_param, :user => {:admin => true} | ||
| 69 | - @user.reload.admin.should be_false | 64 | + expect { |
| 65 | + put :update, :id => user.to_param, :user => {:admin => true} | ||
| 66 | + }.to_not change { | ||
| 67 | + user.reload.admin | ||
| 68 | + }.from(false) | ||
| 70 | end | 69 | end |
| 71 | 70 | ||
| 72 | it "should be able to set per_page option" do | 71 | it "should be able to set per_page option" do |
| 73 | - put :update, :id => @user.to_param, :user => {:per_page => 555} | ||
| 74 | - @user.reload.per_page.should == 555 | 72 | + put :update, :id => user.to_param, :user => {:per_page => 555} |
| 73 | + user.reload.per_page.should == 555 | ||
| 75 | end | 74 | end |
| 76 | 75 | ||
| 77 | it "should be able to set time_zone option" do | 76 | it "should be able to set time_zone option" do |
| 78 | - put :update, :id => @user.to_param, :user => {:time_zone => "Warsaw"} | ||
| 79 | - @user.reload.time_zone.should == "Warsaw" | 77 | + put :update, :id => user.to_param, :user => {:time_zone => "Warsaw"} |
| 78 | + user.reload.time_zone.should == "Warsaw" | ||
| 79 | + end | ||
| 80 | + | ||
| 81 | + it "should be able to not set github_login option" do | ||
| 82 | + put :update, :id => user.to_param, :user => {:github_login => " "} | ||
| 83 | + user.reload.github_login.should == nil | ||
| 80 | end | 84 | end |
| 81 | 85 | ||
| 82 | it "should be able to set github_login option" do | 86 | it "should be able to set github_login option" do |
| 83 | - put :update, :id => @user.to_param, :user => {:github_login => "awesome_name"} | ||
| 84 | - @user.reload.github_login.should == "awesome_name" | 87 | + put :update, :id => user.to_param, :user => {:github_login => "awesome_name"} |
| 88 | + user.reload.github_login.should == "awesome_name" | ||
| 85 | end | 89 | end |
| 86 | end | 90 | end |
| 87 | 91 | ||
| 88 | context "when the update is unsuccessful" do | 92 | context "when the update is unsuccessful" do |
| 89 | it "renders the edit page" do | 93 | it "renders the edit page" do |
| 90 | - put :update, :id => @user.to_param, :user => {:name => nil} | 94 | + put :update, :id => user.to_param, :user => {:name => nil} |
| 91 | response.should render_template(:edit) | 95 | response.should render_template(:edit) |
| 92 | end | 96 | end |
| 93 | end | 97 | end |
| @@ -96,81 +100,82 @@ describe UsersController do | @@ -96,81 +100,82 @@ describe UsersController do | ||
| 96 | 100 | ||
| 97 | context 'Signed in as an admin' do | 101 | context 'Signed in as an admin' do |
| 98 | before do | 102 | before do |
| 99 | - @user = Fabricate(:admin) | ||
| 100 | - sign_in @user | 103 | + sign_in admin |
| 101 | end | 104 | end |
| 102 | 105 | ||
| 103 | context "GET /users" do | 106 | context "GET /users" do |
| 107 | + | ||
| 104 | it 'paginates all users' do | 108 | it 'paginates all users' do |
| 105 | - @user.update_attribute :per_page, 2 | ||
| 106 | - users = 3.times { Fabricate(:user) } | 109 | + admin.update_attribute :per_page, 2 |
| 110 | + users = 3.times { | ||
| 111 | + Fabricate(:user) | ||
| 112 | + } | ||
| 107 | get :index | 113 | get :index |
| 108 | - assigns(:users).to_a.size.should == 2 | 114 | + controller.users.to_a.size.should == 2 |
| 109 | end | 115 | end |
| 116 | + | ||
| 110 | end | 117 | end |
| 111 | 118 | ||
| 112 | context "GET /users/:id" do | 119 | context "GET /users/:id" do |
| 113 | it 'finds the user' do | 120 | it 'finds the user' do |
| 114 | - user = Fabricate(:user) | ||
| 115 | get :show, :id => user.id | 121 | get :show, :id => user.id |
| 116 | - assigns(:user).should == user | 122 | + controller.user.should == user |
| 117 | end | 123 | end |
| 118 | end | 124 | end |
| 119 | 125 | ||
| 120 | context "GET /users/new" do | 126 | context "GET /users/new" do |
| 121 | it 'assigns a new user' do | 127 | it 'assigns a new user' do |
| 122 | get :new | 128 | get :new |
| 123 | - assigns(:user).should be_a(User) | ||
| 124 | - assigns(:user).should be_new_record | 129 | + controller.user.should be_a(User) |
| 130 | + controller.user.should be_new_record | ||
| 125 | end | 131 | end |
| 126 | end | 132 | end |
| 127 | 133 | ||
| 128 | context "GET /users/:id/edit" do | 134 | context "GET /users/:id/edit" do |
| 129 | it 'finds the user' do | 135 | it 'finds the user' do |
| 130 | - user = Fabricate(:user) | ||
| 131 | get :edit, :id => user.id | 136 | get :edit, :id => user.id |
| 132 | - assigns(:user).should == user | 137 | + controller.user.should == user |
| 133 | end | 138 | end |
| 134 | end | 139 | end |
| 135 | 140 | ||
| 136 | context "POST /users" do | 141 | context "POST /users" do |
| 137 | context "when the create is successful" do | 142 | context "when the create is successful" do |
| 138 | - before do | ||
| 139 | - @attrs = {:user => Fabricate.attributes_for(:user)} | ||
| 140 | - end | 143 | + let(:attrs) { {:user => Fabricate.attributes_for(:user)} } |
| 141 | 144 | ||
| 142 | it "sets a message to display" do | 145 | it "sets a message to display" do |
| 143 | - post :create, @attrs | 146 | + post :create, attrs |
| 144 | request.flash[:success].should include('part of the team') | 147 | request.flash[:success].should include('part of the team') |
| 145 | end | 148 | end |
| 146 | 149 | ||
| 147 | it "redirects to the user's page" do | 150 | it "redirects to the user's page" do |
| 148 | - post :create, @attrs | ||
| 149 | - response.should redirect_to(user_path(assigns(:user))) | 151 | + post :create, attrs |
| 152 | + response.should redirect_to(user_path(controller.user)) | ||
| 150 | end | 153 | end |
| 151 | 154 | ||
| 152 | it "should be able to create admin" do | 155 | it "should be able to create admin" do |
| 153 | - @attrs[:user][:admin] = true | ||
| 154 | - post :create, @attrs | 156 | + attrs[:user][:admin] = true |
| 157 | + post :create, attrs | ||
| 155 | response.should be_redirect | 158 | response.should be_redirect |
| 156 | - User.find(assigns(:user).to_param).admin.should be_true | 159 | + User.find(controller.user.to_param).admin.should be_true |
| 157 | end | 160 | end |
| 158 | 161 | ||
| 159 | it "should has auth token" do | 162 | it "should has auth token" do |
| 160 | - post :create, @attrs | 163 | + post :create, attrs |
| 161 | User.last.authentication_token.should_not be_blank | 164 | User.last.authentication_token.should_not be_blank |
| 162 | end | 165 | end |
| 163 | end | 166 | end |
| 164 | 167 | ||
| 165 | context "when the create is unsuccessful" do | 168 | context "when the create is unsuccessful" do |
| 169 | + let(:user) { | ||
| 170 | + Struct.new(:admin, :attributes).new(true, {}) | ||
| 171 | + } | ||
| 166 | before do | 172 | before do |
| 167 | - @user = Fabricate(:user) | ||
| 168 | - User.should_receive(:new).and_return(@user) | ||
| 169 | - @user.should_receive(:save).and_return(false) | 173 | + User.should_receive(:new).and_return(user) |
| 174 | + user.should_receive(:save).and_return(false) | ||
| 170 | end | 175 | end |
| 171 | 176 | ||
| 172 | it "renders the new page" do | 177 | it "renders the new page" do |
| 173 | - post :create | 178 | + post :create, :user => { :username => 'foo' } |
| 174 | response.should render_template(:new) | 179 | response.should render_template(:new) |
| 175 | end | 180 | end |
| 176 | end | 181 | end |
| @@ -178,59 +183,85 @@ describe UsersController do | @@ -178,59 +183,85 @@ describe UsersController do | ||
| 178 | 183 | ||
| 179 | context "PUT /users/:id" do | 184 | context "PUT /users/:id" do |
| 180 | context "when the update is successful" do | 185 | context "when the update is successful" do |
| 181 | - before do | ||
| 182 | - @user = Fabricate(:user) | ||
| 183 | - end | ||
| 184 | - | ||
| 185 | - it "sets a message to display" do | ||
| 186 | - put :update, :id => @user.to_param, :user => {:name => 'Kermit'} | ||
| 187 | - request.flash[:success].should include('updated') | ||
| 188 | - end | ||
| 189 | - | ||
| 190 | - it "redirects to the user's page" do | ||
| 191 | - put :update, :id => @user.to_param, :user => {:name => 'Kermit'} | ||
| 192 | - response.should redirect_to(user_path(@user)) | ||
| 193 | - end | ||
| 194 | - | ||
| 195 | - it "should be able to make user an admin" do | ||
| 196 | - put :update, :id => @user.to_param, :user => {:admin => true} | ||
| 197 | - response.should be_redirect | ||
| 198 | - User.find(assigns(:user).to_param).admin.should be_true | 186 | + before { |
| 187 | + put :update, :id => user.to_param, :user => user_params | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + context "with normal params" do | ||
| 191 | + let(:user_params) { {:name => 'Kermit'} } | ||
| 192 | + it "sets a message to display" do | ||
| 193 | + expect(request.flash[:success]).to eq I18n.t('controllers.users.flash.update.success', :name => user.name) | ||
| 194 | + expect(response).to redirect_to(user_path(user)) | ||
| 195 | + end | ||
| 199 | end | 196 | end |
| 200 | end | 197 | end |
| 201 | - | ||
| 202 | context "when the update is unsuccessful" do | 198 | context "when the update is unsuccessful" do |
| 203 | - before do | ||
| 204 | - @user = Fabricate(:user) | ||
| 205 | - end | ||
| 206 | 199 | ||
| 207 | it "renders the edit page" do | 200 | it "renders the edit page" do |
| 208 | - put :update, :id => @user.to_param, :user => {:name => nil} | 201 | + put :update, :id => user.to_param, :user => {:name => nil} |
| 209 | response.should render_template(:edit) | 202 | response.should render_template(:edit) |
| 210 | end | 203 | end |
| 211 | end | 204 | end |
| 212 | end | 205 | end |
| 213 | 206 | ||
| 214 | context "DELETE /users/:id" do | 207 | context "DELETE /users/:id" do |
| 215 | - before do | ||
| 216 | - @user = Fabricate(:user) | 208 | + |
| 209 | + context "with a destroy success" do | ||
| 210 | + let(:user_destroy) { double(:destroy => true) } | ||
| 211 | + | ||
| 212 | + before { | ||
| 213 | + UserDestroy.should_receive(:new).with(user).and_return(user_destroy) | ||
| 214 | + delete :destroy, :id => user.id | ||
| 215 | + } | ||
| 216 | + | ||
| 217 | + it 'should destroy user' do | ||
| 218 | + expect(request.flash[:success]).to eq I18n.t('controllers.users.flash.destroy.success', :name => user.name) | ||
| 219 | + response.should redirect_to(users_path) | ||
| 220 | + end | ||
| 217 | end | 221 | end |
| 218 | 222 | ||
| 219 | - it "destroys the user" do | ||
| 220 | - delete :destroy, :id => @user.id | ||
| 221 | - User.where(:id => @user.id).first.should be_nil | 223 | + context "with trying destroy himself" do |
| 224 | + before { | ||
| 225 | + UserDestroy.should_not_receive(:new) | ||
| 226 | + delete :destroy, :id => admin.id | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + it 'should not destroy user' do | ||
| 230 | + response.should redirect_to(users_path) | ||
| 231 | + expect(request.flash[:error]).to eq I18n.t('controllers.users.flash.destroy.error') | ||
| 232 | + end | ||
| 222 | end | 233 | end |
| 234 | + end | ||
| 223 | 235 | ||
| 224 | - it "redirects to the users index page" do | ||
| 225 | - delete :destroy, :id => @user.id | ||
| 226 | - response.should redirect_to(users_path) | 236 | + describe "#user_params" do |
| 237 | + context "with current user not admin" do | ||
| 238 | + before { | ||
| 239 | + controller.stub(:current_user).and_return(user) | ||
| 240 | + controller.stub(:params).and_return(ActionController::Parameters.new(user_param)) | ||
| 241 | + } | ||
| 242 | + let(:user_param) { {'user' => { :name => 'foo', :admin => true }} } | ||
| 243 | + it 'not have admin field' do | ||
| 244 | + expect(controller.send(:user_params)).to eq ({'name' => 'foo'}) | ||
| 245 | + end | ||
| 246 | + context "with password and password_confirmation empty?" do | ||
| 247 | + let(:user_param) { {'user' => { :name => 'foo', 'password' => '', 'password_confirmation' => '' }} } | ||
| 248 | + it 'not have password and password_confirmation field' do | ||
| 249 | + expect(controller.send(:user_params)).to eq ({'name' => 'foo'}) | ||
| 250 | + end | ||
| 251 | + end | ||
| 227 | end | 252 | end |
| 228 | 253 | ||
| 229 | - it "sets a message to display" do | ||
| 230 | - delete :destroy, :id => @user.id | ||
| 231 | - request.flash[:success].should include('no longer part of your team') | 254 | + context "with current user admin" do |
| 255 | + it 'have admin field' | ||
| 256 | + context "with password and password_confirmation empty?" do | ||
| 257 | + it 'not have password and password_confirmation field' | ||
| 258 | + end | ||
| 259 | + context "on his own user" do | ||
| 260 | + it 'not have admin field' | ||
| 261 | + end | ||
| 232 | end | 262 | end |
| 233 | end | 263 | end |
| 264 | + | ||
| 234 | end | 265 | end |
| 235 | 266 | ||
| 236 | end | 267 | end |
spec/fabricators/app_fabricator.rb
| 1 | Fabricator(:app) do | 1 | Fabricator(:app) do |
| 2 | name { sequence(:app_name){|n| "App ##{n}"} } | 2 | name { sequence(:app_name){|n| "App ##{n}"} } |
| 3 | + repository_branch 'master' | ||
| 3 | end | 4 | end |
| 4 | 5 | ||
| 5 | Fabricator(:app_with_watcher, :from => :app) do | 6 | Fabricator(:app_with_watcher, :from => :app) do |
| 6 | - watchers(:count => 1) { |parent, i| Fabricate.build(:watcher, :app => parent) } | 7 | + watchers(:count => 1) { |parent, i| |
| 8 | + Fabricate.build(:watcher, :app => parent) | ||
| 9 | + } | ||
| 7 | end | 10 | end |
| 8 | 11 | ||
| 9 | Fabricator(:watcher) do | 12 | Fabricator(:watcher) do |
spec/fabricators/err_fabricator.rb
spec/fabricators/issue_tracker_fabricator.rb
| @@ -33,3 +33,8 @@ Fabricator :bitbucket_issues_tracker, :from => :issue_tracker, :class_name => "I | @@ -33,3 +33,8 @@ Fabricator :bitbucket_issues_tracker, :from => :issue_tracker, :class_name => "I | ||
| 33 | project_id 'password' | 33 | project_id 'password' |
| 34 | api_token 'test_username' | 34 | api_token 'test_username' |
| 35 | end | 35 | end |
| 36 | + | ||
| 37 | +Fabricator :unfuddle_issues_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::UnfuddleTracker" do | ||
| 38 | + account 'test' | ||
| 39 | + project_id 15 | ||
| 40 | +end |
spec/fabricators/notification_service_fabricator.rb
| @@ -3,8 +3,15 @@ Fabricator :notification_service do | @@ -3,8 +3,15 @@ Fabricator :notification_service do | ||
| 3 | room_id { sequence :word } | 3 | room_id { sequence :word } |
| 4 | api_token { sequence :word } | 4 | api_token { sequence :word } |
| 5 | subdomain { sequence :word } | 5 | subdomain { sequence :word } |
| 6 | + notify_at_notices { sequence { |a| [0]} } | ||
| 6 | end | 7 | end |
| 7 | 8 | ||
| 8 | -%w(campfire gtalk hipchat hoiio pushover).each do |t| | 9 | +Fabricator :gtalk_notification_service, :from => :notification_service, :class_name => "NotificationService::GtalkService" do |
| 10 | + user_id { sequence :word } | ||
| 11 | + service_url { sequence :word } | ||
| 12 | + service { sequence :word } | ||
| 13 | +end | ||
| 14 | + | ||
| 15 | +%w(campfire flowdock hipchat hoiio hubot pushover webhook).each do |t| | ||
| 9 | Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service" | 16 | Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service" |
| 10 | end | 17 | end |
spec/fabricators/problem_fabricator.rb
| 1 | Fabricator(:problem) do | 1 | Fabricator(:problem) do |
| 2 | app! { Fabricate(:app) } | 2 | app! { Fabricate(:app) } |
| 3 | comments { [] } | 3 | comments { [] } |
| 4 | + error_class 'FooError' | ||
| 5 | + environment 'production' | ||
| 4 | end | 6 | end |
| 5 | 7 | ||
| 6 | Fabricator(:problem_with_comments, :from => :problem) do | 8 | Fabricator(:problem_with_comments, :from => :problem) do |
| @@ -10,3 +12,13 @@ Fabricator(:problem_with_comments, :from => :problem) do | @@ -10,3 +12,13 @@ Fabricator(:problem_with_comments, :from => :problem) do | ||
| 10 | end | 12 | end |
| 11 | } | 13 | } |
| 12 | end | 14 | end |
| 15 | + | ||
| 16 | +Fabricator(:problem_with_errs, :from => :problem) do | ||
| 17 | + after_create { |parent| | ||
| 18 | + 3.times do | ||
| 19 | + Fabricate(:err, :problem => parent) | ||
| 20 | + end | ||
| 21 | + } | ||
| 22 | +end | ||
| 23 | + | ||
| 24 | + |
spec/fixtures/hoptoad_test_notice.xml
| 1 | <?xml version="1.0" encoding="UTF-8"?> | 1 | <?xml version="1.0" encoding="UTF-8"?> |
| 2 | -<notice version="2.3"> | 2 | +<notice version="2.4"> |
| 3 | <api-key>APIKEY</api-key> | 3 | <api-key>APIKEY</api-key> |
| 4 | <notifier> | 4 | <notifier> |
| 5 | <name>Hoptoad Notifier</name> | 5 | <name>Hoptoad Notifier</name> |
| 6 | <version>2.3.2</version> | 6 | <version>2.3.2</version> |
| 7 | <url>http://hoptoadapp.com</url> | 7 | <url>http://hoptoadapp.com</url> |
| 8 | </notifier> | 8 | </notifier> |
| 9 | + <framework>Rails: 3.2.11</framework> | ||
| 9 | <error> | 10 | <error> |
| 10 | <class>HoptoadTestingException</class> | 11 | <class>HoptoadTestingException</class> |
| 11 | - <message>HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.</message> | 12 | + <message><![CDATA[HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works & works well.]]></message> |
| 12 | <backtrace> | 13 | <backtrace> |
| 13 | <line number="425" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run__2115867319__process_action__262109504__callbacks"/> | 14 | <line number="425" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run__2115867319__process_action__262109504__callbacks"/> |
| 14 | <line number="404" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="send"/> | 15 | <line number="404" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="send"/> |
spec/fixtures/hoptoad_test_notice_without_line_of_backtrace.xml
0 → 100644
| @@ -0,0 +1,73 @@ | @@ -0,0 +1,73 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<notice version="2.0"> | ||
| 3 | + <api-key>APIKEY</api-key> | ||
| 4 | + <notifier> | ||
| 5 | + <name>Hoptoad Notifier</name> | ||
| 6 | + <version>2.3.2</version> | ||
| 7 | + <url>http://hoptoadapp.com</url> | ||
| 8 | + </notifier> | ||
| 9 | + <error> | ||
| 10 | + <class>HoptoadTestingException</class> | ||
| 11 | + <message>HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.</message> | ||
| 12 | + <backtrace></backtrace> | ||
| 13 | + </error> | ||
| 14 | + <request> | ||
| 15 | + <url>http://example.org/verify</url> | ||
| 16 | + <component>application</component> | ||
| 17 | + <action>verify</action> | ||
| 18 | + <params> | ||
| 19 | + <var key="action">verify</var> | ||
| 20 | + <var key="controller">application</var> | ||
| 21 | + </params> | ||
| 22 | + <cgi-data> | ||
| 23 | + <var key="rack.session"/> | ||
| 24 | + <var key="action_dispatch.request.formats">text/html</var> | ||
| 25 | + <var key="action_dispatch.request.parameters"> | ||
| 26 | + <var key="action">verify</var> | ||
| 27 | + <var key="controller">application</var> | ||
| 28 | + </var> | ||
| 29 | + <var key="SERVER_NAME">example.org</var> | ||
| 30 | + <var key="rack.url_scheme">http</var> | ||
| 31 | + <var key="action_dispatch.remote_ip"/> | ||
| 32 | + <var key="CONTENT_LENGTH">0</var> | ||
| 33 | + <var key="rack.errors">#<StringIO:0x103d9dec0></var> | ||
| 34 | + <var key="action_dispatch.request.unsigned_session_cookie"/> | ||
| 35 | + <var key="action_dispatch.request.query_parameters"/> | ||
| 36 | + <var key="HTTPS">off</var> | ||
| 37 | + <var key="rack.run_once">false</var> | ||
| 38 | + <var key="PATH_INFO">/verify</var> | ||
| 39 | + <var key="action_dispatch.secret_token">994f235e3372684bc736dd11842b754d2ddcffc8c2958d33a29527c3217becd6655fa4653a318bc7c34131f9baf2acc0c424ed07e48e0e5e87c6cd34d711e985</var> | ||
| 40 | + <var key="rack.version">11</var> | ||
| 41 | + <var key="SCRIPT_NAME"/> | ||
| 42 | + <var key="action_dispatch.request.path_parameters"> | ||
| 43 | + <var key="action">verify</var> | ||
| 44 | + <var key="controller">application</var> | ||
| 45 | + </var> | ||
| 46 | + <var key="rack.multithread">false</var> | ||
| 47 | + <var key="action_dispatch.parameter_filter">password</var> | ||
| 48 | + <var key="action_dispatch.cookies"/> | ||
| 49 | + <var key="action_dispatch.request.request_parameters"/> | ||
| 50 | + <var key="rack.multiprocess">true</var> | ||
| 51 | + <var key="rack.request.query_hash"/> | ||
| 52 | + <var key="SERVER_PORT">80</var> | ||
| 53 | + <var key="REQUEST_METHOD">GET</var> | ||
| 54 | + <var key="action_controller.instance">#<ApplicationController:0x103d2f560></var> | ||
| 55 | + <var key="rack.session.options"> | ||
| 56 | + <var key="secure">false</var> | ||
| 57 | + <var key="httponly">true</var> | ||
| 58 | + <var key="path">/</var> | ||
| 59 | + <var key="expire_after"/> | ||
| 60 | + <var key="domain"/> | ||
| 61 | + <var key="id"/> | ||
| 62 | + </var> | ||
| 63 | + <var key="rack.input">#<StringIO:0x103d9dc90></var> | ||
| 64 | + <var key="action_dispatch.request.content_type"/> | ||
| 65 | + <var key="rack.request.query_string"/> | ||
| 66 | + <var key="QUERY_STRING"/> | ||
| 67 | + </cgi-data> | ||
| 68 | + </request> | ||
| 69 | + <server-environment> | ||
| 70 | + <project-root>/path/to/sample/project</project-root> | ||
| 71 | + <environment-name>development</environment-name> | ||
| 72 | + </server-environment> | ||
| 73 | +</notice> |
spec/helpers/application_helper_spec.rb
| @@ -10,3 +10,12 @@ require 'spec_helper' | @@ -10,3 +10,12 @@ require 'spec_helper' | ||
| 10 | # end | 10 | # end |
| 11 | # end | 11 | # end |
| 12 | # end | 12 | # end |
| 13 | + | ||
| 14 | +describe ApplicationHelper do | ||
| 15 | + let(:notice) { Fabricate(:notice) } | ||
| 16 | + describe "#generate_problem_ical" do | ||
| 17 | + it 'return the ical format' do | ||
| 18 | + helper.generate_problem_ical([notice]) | ||
| 19 | + end | ||
| 20 | + end | ||
| 21 | +end |
| @@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe BacktraceLineHelper do | ||
| 4 | + describe "in app lines" do | ||
| 5 | + let(:notice) do | ||
| 6 | + Fabricate.build(:notice, :backtrace => | ||
| 7 | + Fabricate.build(:backtrace, :lines => [ | ||
| 8 | + Fabricate.build(:backtrace_line, :file => "[PROJECT_ROOT]/path/to/asset.js") | ||
| 9 | + ]) | ||
| 10 | + ) | ||
| 11 | + end | ||
| 12 | + | ||
| 13 | + describe '#link_to_source_file' do | ||
| 14 | + it 'still returns text for in app file and line number when no repo is configured' do | ||
| 15 | + result = link_to_source_file(notice.backtrace.lines.first) { haml_concat "link text" } | ||
| 16 | + result.strip.should == 'link text' | ||
| 17 | + end | ||
| 18 | + end | ||
| 19 | + end | ||
| 20 | +end |
spec/helpers/problems_helper_spec.rb
| @@ -31,5 +31,40 @@ describe ProblemsHelper do | @@ -31,5 +31,40 @@ describe ProblemsHelper do | ||
| 31 | helper.gravatar_tag(email, :d => 'retro', :s => 48).should eq(expected) | 31 | helper.gravatar_tag(email, :d => 'retro', :s => 48).should eq(expected) |
| 32 | end | 32 | end |
| 33 | end | 33 | end |
| 34 | + | ||
| 35 | + context "no email" do | ||
| 36 | + it "should not render the tag" do | ||
| 37 | + helper.gravatar_tag(nil).should be_nil | ||
| 38 | + end | ||
| 39 | + end | ||
| 40 | + end | ||
| 41 | + | ||
| 42 | + describe "#gravatar_url" do | ||
| 43 | + context "no email" do | ||
| 44 | + let(:email) { nil } | ||
| 45 | + | ||
| 46 | + it "should return nil" do | ||
| 47 | + helper.gravatar_url(email).should be_nil | ||
| 48 | + end | ||
| 49 | + end | ||
| 50 | + | ||
| 51 | + context "without ssl" do | ||
| 52 | + let(:email) { "gravatar@example.com" } | ||
| 53 | + let(:email_hash) { Digest::MD5.hexdigest email } | ||
| 54 | + | ||
| 55 | + it "should return the http url" do | ||
| 56 | + helper.gravatar_url(email).should eq("http://www.gravatar.com/avatar/#{email_hash}?d=identicon") | ||
| 57 | + end | ||
| 58 | + end | ||
| 59 | + | ||
| 60 | + context "with ssl" do | ||
| 61 | + let(:email) { "gravatar@example.com" } | ||
| 62 | + let(:email_hash) { Digest::MD5.hexdigest email } | ||
| 63 | + | ||
| 64 | + it "should return the http url" do | ||
| 65 | + ActionController::TestRequest.any_instance.stub :ssl? => true | ||
| 66 | + helper.gravatar_url(email).should eq("https://secure.gravatar.com/avatar/#{email_hash}?d=identicon") | ||
| 67 | + end | ||
| 68 | + end | ||
| 34 | end | 69 | end |
| 35 | end | 70 | end |
spec/interactors/problem_destroy_spec.rb
| @@ -8,8 +8,8 @@ describe ProblemDestroy do | @@ -8,8 +8,8 @@ describe ProblemDestroy do | ||
| 8 | context "in unit way" do | 8 | context "in unit way" do |
| 9 | let(:problem) { | 9 | let(:problem) { |
| 10 | problem = Problem.new | 10 | problem = Problem.new |
| 11 | - problem.stub(:errs).and_return(mock(:criteria, :only => [err_1, err_2])) | ||
| 12 | - problem.stub(:comments).and_return(mock(:criteria, :only => [comment_1, comment_2])) | 11 | + problem.stub(:errs).and_return(double(:criteria, :only => [err_1, err_2])) |
| 12 | + problem.stub(:comments).and_return(double(:criteria, :only => [comment_1, comment_2])) | ||
| 13 | problem.stub(:delete) | 13 | problem.stub(:delete) |
| 14 | problem | 14 | problem |
| 15 | } | 15 | } |
| @@ -32,17 +32,17 @@ describe ProblemDestroy do | @@ -32,17 +32,17 @@ describe ProblemDestroy do | ||
| 32 | end | 32 | end |
| 33 | 33 | ||
| 34 | it 'delete all errs associate' do | 34 | it 'delete all errs associate' do |
| 35 | - Err.collection.should_receive(:remove).with(:_id => { '$in' => [err_1.id, err_2.id] }) | 35 | + Err.should_receive(:delete_all).with(:_id => { '$in' => [err_1.id, err_2.id] }) |
| 36 | problem_destroy.execute | 36 | problem_destroy.execute |
| 37 | end | 37 | end |
| 38 | 38 | ||
| 39 | it 'delete all comments associate' do | 39 | it 'delete all comments associate' do |
| 40 | - Comment.collection.should_receive(:remove).with(:_id => { '$in' => [comment_1.id, comment_2.id] }) | 40 | + Comment.should_receive(:delete_all).with(:_id => { '$in' => [comment_1.id, comment_2.id] }) |
| 41 | problem_destroy.execute | 41 | problem_destroy.execute |
| 42 | end | 42 | end |
| 43 | 43 | ||
| 44 | it 'delete all notice of associate to this errs' do | 44 | it 'delete all notice of associate to this errs' do |
| 45 | - Notice.collection.should_receive(:remove).with({:err_id => { '$in' => [err_1.id, err_2.id] }}) | 45 | + Notice.should_receive(:delete_all).with({:err_id => { '$in' => [err_1.id, err_2.id] }}) |
| 46 | problem_destroy.execute | 46 | problem_destroy.execute |
| 47 | end | 47 | end |
| 48 | end | 48 | end |
| @@ -0,0 +1,62 @@ | @@ -0,0 +1,62 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe ProblemMerge do | ||
| 4 | + let(:problem) { Fabricate(:problem_with_errs) } | ||
| 5 | + let(:problem_1) { Fabricate(:problem_with_errs) } | ||
| 6 | + | ||
| 7 | + describe "#initialize" do | ||
| 8 | + it 'failed if less than 2 uniq problem pass in args' do | ||
| 9 | + expect { | ||
| 10 | + ProblemMerge.new(problem) | ||
| 11 | + }.to raise_error(ArgumentError) | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + it 'extract first problem like merged_problem' do | ||
| 15 | + problem_merge = ProblemMerge.new(problem, problem, problem_1) | ||
| 16 | + expect(problem_merge.merged_problem).to eql problem | ||
| 17 | + end | ||
| 18 | + it 'extract other problem like child_problems' do | ||
| 19 | + problem_merge = ProblemMerge.new(problem, problem, problem_1) | ||
| 20 | + expect(problem_merge.child_problems).to eql [problem_1] | ||
| 21 | + end | ||
| 22 | + end | ||
| 23 | + | ||
| 24 | + describe "#merge" do | ||
| 25 | + let!(:problem_merge) { | ||
| 26 | + ProblemMerge.new(problem, problem_1) | ||
| 27 | + } | ||
| 28 | + let(:first_errs) { problem.errs } | ||
| 29 | + let(:merged_errs) { problem_1.errs } | ||
| 30 | + let!(:notice) { Fabricate(:notice, :err => first_errs.first) } | ||
| 31 | + let!(:notice_1) { Fabricate(:notice, :err => merged_errs.first) } | ||
| 32 | + | ||
| 33 | + it 'delete one of problem' do | ||
| 34 | + expect { | ||
| 35 | + problem_merge.merge | ||
| 36 | + }.to change(Problem, :count).by(-1) | ||
| 37 | + end | ||
| 38 | + | ||
| 39 | + it 'move all err in one problem' do | ||
| 40 | + problem_merge.merge | ||
| 41 | + expect(problem.reload.errs.map(&:id).sort).to eq (first_errs | merged_errs).map(&:id).sort | ||
| 42 | + end | ||
| 43 | + | ||
| 44 | + it 'update problem cache' do | ||
| 45 | + ProblemUpdaterCache.should_receive(:new).with(problem).and_return(double(:update => true)) | ||
| 46 | + problem_merge.merge | ||
| 47 | + end | ||
| 48 | + | ||
| 49 | + context "with problem with comment" do | ||
| 50 | + let!(:comment) { Fabricate(:comment, :err => problem ) } | ||
| 51 | + let!(:comment_2) { Fabricate(:comment, :err => problem_1, :user => comment.user ) } | ||
| 52 | + it 'merge comment' do | ||
| 53 | + expect { | ||
| 54 | + problem_merge.merge | ||
| 55 | + }.to change{ | ||
| 56 | + problem.comments.size | ||
| 57 | + }.from(1).to(2) | ||
| 58 | + expect(comment_2.reload.err).to eq problem | ||
| 59 | + end | ||
| 60 | + end | ||
| 61 | + end | ||
| 62 | +end |
| @@ -0,0 +1,147 @@ | @@ -0,0 +1,147 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe ProblemUpdaterCache do | ||
| 4 | + let(:problem) { Fabricate(:problem_with_errs) } | ||
| 5 | + let(:first_errs) { problem.errs } | ||
| 6 | + let!(:notice) { Fabricate(:notice, :err => first_errs.first) } | ||
| 7 | + | ||
| 8 | + describe "#update" do | ||
| 9 | + context "without notice pass args" do | ||
| 10 | + before do | ||
| 11 | + problem.update_attribute(:notices_count, 0) | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + it 'update the notice_count' do | ||
| 15 | + expect { | ||
| 16 | + ProblemUpdaterCache.new(problem).update | ||
| 17 | + }.to change{ | ||
| 18 | + problem.notices_count | ||
| 19 | + }.from(0).to(1) | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + context "with only one notice" do | ||
| 23 | + before do | ||
| 24 | + problem.update_attributes!(:messages => {}) | ||
| 25 | + ProblemUpdaterCache.new(problem).update | ||
| 26 | + end | ||
| 27 | + | ||
| 28 | + it 'update information about this notice' do | ||
| 29 | + expect(problem.message).to eq notice.message | ||
| 30 | + expect(problem.where).to eq notice.where | ||
| 31 | + end | ||
| 32 | + | ||
| 33 | + it 'update first_notice_at' do | ||
| 34 | + expect(problem.first_notice_at).to eq notice.reload.created_at | ||
| 35 | + end | ||
| 36 | + | ||
| 37 | + it 'update last_notice_at' do | ||
| 38 | + expect(problem.last_notice_at).to eq notice.reload.created_at | ||
| 39 | + end | ||
| 40 | + | ||
| 41 | + it 'update stats messages' do | ||
| 42 | + expect(problem.messages).to eq({ | ||
| 43 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 1} | ||
| 44 | + }) | ||
| 45 | + end | ||
| 46 | + | ||
| 47 | + it 'update stats hosts' do | ||
| 48 | + expect(problem.hosts).to eq({ | ||
| 49 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 1} | ||
| 50 | + }) | ||
| 51 | + end | ||
| 52 | + | ||
| 53 | + it 'update stats user_agents' do | ||
| 54 | + expect(problem.user_agents).to eq({ | ||
| 55 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 1} | ||
| 56 | + }) | ||
| 57 | + end | ||
| 58 | + end | ||
| 59 | + | ||
| 60 | + context "with several notices" do | ||
| 61 | + let!(:notice_2) { Fabricate(:notice, :err => first_errs.first) } | ||
| 62 | + let!(:notice_3) { Fabricate(:notice, :err => first_errs.first) } | ||
| 63 | + before do | ||
| 64 | + problem.update_attributes!(:messages => {}) | ||
| 65 | + ProblemUpdaterCache.new(problem).update | ||
| 66 | + end | ||
| 67 | + it 'update information about this notice' do | ||
| 68 | + expect(problem.message).to eq notice.message | ||
| 69 | + expect(problem.where).to eq notice.where | ||
| 70 | + end | ||
| 71 | + | ||
| 72 | + it 'update first_notice_at' do | ||
| 73 | + expect(problem.first_notice_at.to_i).to be_within(1).of(notice.created_at.to_i) | ||
| 74 | + end | ||
| 75 | + | ||
| 76 | + it 'update last_notice_at' do | ||
| 77 | + expect(problem.last_notice_at.to_i).to be_within(1).of(notice.created_at.to_i) | ||
| 78 | + end | ||
| 79 | + | ||
| 80 | + it 'update stats messages' do | ||
| 81 | + expect(problem.messages).to eq({ | ||
| 82 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 3} | ||
| 83 | + }) | ||
| 84 | + end | ||
| 85 | + | ||
| 86 | + it 'update stats hosts' do | ||
| 87 | + expect(problem.hosts).to eq({ | ||
| 88 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 3} | ||
| 89 | + }) | ||
| 90 | + end | ||
| 91 | + | ||
| 92 | + it 'update stats user_agents' do | ||
| 93 | + expect(problem.user_agents).to eq({ | ||
| 94 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 3} | ||
| 95 | + }) | ||
| 96 | + end | ||
| 97 | + | ||
| 98 | + end | ||
| 99 | + end | ||
| 100 | + | ||
| 101 | + context "with notice pass in args" do | ||
| 102 | + | ||
| 103 | + before do | ||
| 104 | + ProblemUpdaterCache.new(problem, notice).update | ||
| 105 | + end | ||
| 106 | + | ||
| 107 | + it 'increase notices_count by 1' do | ||
| 108 | + expect { | ||
| 109 | + ProblemUpdaterCache.new(problem, notice).update | ||
| 110 | + }.to change{ | ||
| 111 | + problem.notices_count | ||
| 112 | + }.by(1) | ||
| 113 | + end | ||
| 114 | + | ||
| 115 | + it 'update information about this notice' do | ||
| 116 | + expect(problem.message).to eq notice.message | ||
| 117 | + expect(problem.where).to eq notice.where | ||
| 118 | + end | ||
| 119 | + | ||
| 120 | + it 'update first_notice_at' do | ||
| 121 | + expect(problem.first_notice_at).to eq notice.created_at | ||
| 122 | + end | ||
| 123 | + | ||
| 124 | + it 'update last_notice_at' do | ||
| 125 | + expect(problem.last_notice_at).to eq notice.created_at | ||
| 126 | + end | ||
| 127 | + | ||
| 128 | + it 'inc stats messages' do | ||
| 129 | + expect(problem.messages).to eq({ | ||
| 130 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 2} | ||
| 131 | + }) | ||
| 132 | + end | ||
| 133 | + | ||
| 134 | + it 'inc stats hosts' do | ||
| 135 | + expect(problem.hosts).to eq({ | ||
| 136 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 2} | ||
| 137 | + }) | ||
| 138 | + end | ||
| 139 | + | ||
| 140 | + it 'inc stats user_agents' do | ||
| 141 | + expect(problem.user_agents).to eq({ | ||
| 142 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 2} | ||
| 143 | + }) | ||
| 144 | + end | ||
| 145 | + end | ||
| 146 | + end | ||
| 147 | +end |
| @@ -0,0 +1,55 @@ | @@ -0,0 +1,55 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe ResolvedProblemClearer do | ||
| 4 | + let(:resolved_problem_clearer) { | ||
| 5 | + ResolvedProblemClearer.new | ||
| 6 | + } | ||
| 7 | + describe "#execute" do | ||
| 8 | + let!(:problems) { | ||
| 9 | + [ | ||
| 10 | + Fabricate(:problem), | ||
| 11 | + Fabricate(:problem), | ||
| 12 | + Fabricate(:problem) | ||
| 13 | + ] | ||
| 14 | + } | ||
| 15 | + context 'without problem resolved' do | ||
| 16 | + it 'do nothing' do | ||
| 17 | + expect { | ||
| 18 | + expect(resolved_problem_clearer.execute).to eq 0 | ||
| 19 | + }.to_not change { | ||
| 20 | + Problem.count | ||
| 21 | + } | ||
| 22 | + end | ||
| 23 | + it 'not repair database' do | ||
| 24 | + Mongoid.default_session.stub(:command).and_call_original | ||
| 25 | + Mongoid.default_session.should_not_receive(:command).with({:repairDatabase => 1}) | ||
| 26 | + resolved_problem_clearer.execute | ||
| 27 | + end | ||
| 28 | + end | ||
| 29 | + | ||
| 30 | + context "with problem resolve" do | ||
| 31 | + before do | ||
| 32 | + Mongoid.default_session.stub(:command).and_call_original | ||
| 33 | + Mongoid.default_session.stub(:command).with({:repairDatabase => 1}) | ||
| 34 | + problems.first.resolve! | ||
| 35 | + problems.second.resolve! | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + it 'delete problem resolve' do | ||
| 39 | + expect { | ||
| 40 | + expect(resolved_problem_clearer.execute).to eq 2 | ||
| 41 | + }.to change { | ||
| 42 | + Problem.count | ||
| 43 | + }.by(-2) | ||
| 44 | + expect(Problem.where(:_id => problems.first.id).first).to be_nil | ||
| 45 | + expect(Problem.where(:_id => problems.second.id).first).to be_nil | ||
| 46 | + end | ||
| 47 | + | ||
| 48 | + it 'repair database' do | ||
| 49 | + Mongoid.default_session.stub(:command).and_call_original | ||
| 50 | + Mongoid.default_session.should_receive(:command).with({:repairDatabase => 1}) | ||
| 51 | + resolved_problem_clearer.execute | ||
| 52 | + end | ||
| 53 | + end | ||
| 54 | + end | ||
| 55 | +end |
| @@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe UserDestroy do | ||
| 4 | + let(:app) { Fabricate( | ||
| 5 | + :app, | ||
| 6 | + :watchers => [ | ||
| 7 | + Fabricate.build(:user_watcher, :user => user) | ||
| 8 | + ]) | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + describe "#destroy" do | ||
| 12 | + let!(:user) { Fabricate(:user) } | ||
| 13 | + it 'should delete user' do | ||
| 14 | + expect { | ||
| 15 | + UserDestroy.new(user).destroy | ||
| 16 | + }.to change(User, :count) | ||
| 17 | + end | ||
| 18 | + | ||
| 19 | + it 'should delete watcher' do | ||
| 20 | + expect { | ||
| 21 | + UserDestroy.new(user).destroy | ||
| 22 | + }.to change{ | ||
| 23 | + app.reload.watchers.where(:user_id => user.id).count | ||
| 24 | + }.from(1).to(0) | ||
| 25 | + end | ||
| 26 | + end | ||
| 27 | +end |
spec/mailers/mailer_spec.rb
| @@ -6,19 +6,76 @@ describe Mailer do | @@ -6,19 +6,76 @@ describe Mailer do | ||
| 6 | include EmailSpec::Matchers | 6 | include EmailSpec::Matchers |
| 7 | 7 | ||
| 8 | let(:notice) { Fabricate(:notice, :message => "class < ActionController::Base") } | 8 | let(:notice) { Fabricate(:notice, :message => "class < ActionController::Base") } |
| 9 | - let!(:email) { Mailer.err_notification(notice).deliver } | 9 | + let!(:user) { Fabricate(:admin) } |
| 10 | + | ||
| 11 | + before do | ||
| 12 | + notice.backtrace.lines.last.update_attributes(:file => "[PROJECT_ROOT]/path/to/file.js") | ||
| 13 | + notice.app.update_attributes( | ||
| 14 | + :asset_host => "http://example.com", | ||
| 15 | + :notify_all_users => true | ||
| 16 | + ) | ||
| 17 | + notice.problem.update_attributes :notices_count => 3 | ||
| 18 | + | ||
| 19 | + @email = Mailer.err_notification(notice).deliver | ||
| 20 | + end | ||
| 10 | 21 | ||
| 11 | it "should send the email" do | 22 | it "should send the email" do |
| 12 | ActionMailer::Base.deliveries.size.should == 1 | 23 | ActionMailer::Base.deliveries.size.should == 1 |
| 13 | end | 24 | end |
| 14 | 25 | ||
| 15 | it "should html-escape the notice's message for the html part" do | 26 | it "should html-escape the notice's message for the html part" do |
| 16 | - email.should have_body_text("class < ActionController::Base") | 27 | + @email.should have_body_text("class < ActionController::Base") |
| 17 | end | 28 | end |
| 18 | 29 | ||
| 19 | it "should have inline css" do | 30 | it "should have inline css" do |
| 20 | - email.should have_body_text('<p class="backtrace" style="') | 31 | + @email.should have_body_text('<p class="backtrace" style="') |
| 32 | + end | ||
| 33 | + | ||
| 34 | + it "should have links to source files" do | ||
| 35 | + @email.should have_body_text('<a href="http://example.com/path/to/file.js" target="_blank">path/to/file.js') | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + it "should have the error count in the subject" do | ||
| 39 | + @email.subject.should =~ /^\(3\) / | ||
| 40 | + end | ||
| 41 | + | ||
| 42 | + context 'with a very long message' do | ||
| 43 | + let(:notice) { Fabricate(:notice, :message => 6.times.collect{|a| "0123456789" }.join('')) } | ||
| 44 | + it "should truncate the long message" do | ||
| 45 | + @email.subject.should =~ / \d{47}\.{3}$/ | ||
| 46 | + end | ||
| 21 | end | 47 | end |
| 22 | end | 48 | end |
| 23 | -end | ||
| 24 | 49 | ||
| 50 | + context "Comment Notification" do | ||
| 51 | + include EmailSpec::Helpers | ||
| 52 | + include EmailSpec::Matchers | ||
| 53 | + | ||
| 54 | + let!(:notice) { Fabricate(:notice) } | ||
| 55 | + let!(:comment) { Fabricate.build(:comment, :err => notice.problem) } | ||
| 56 | + let!(:watcher) { Fabricate(:watcher, :app => comment.app) } | ||
| 57 | + let(:recipients) { ['recipient@example.com', 'another@example.com']} | ||
| 58 | + | ||
| 59 | + before do | ||
| 60 | + comment.stub(:notification_recipients).and_return(recipients) | ||
| 61 | + Fabricate(:notice, :err => notice.err) | ||
| 62 | + @email = Mailer.comment_notification(comment).deliver | ||
| 63 | + end | ||
| 64 | + | ||
| 65 | + it "should send the email" do | ||
| 66 | + ActionMailer::Base.deliveries.size.should == 1 | ||
| 67 | + end | ||
| 68 | + | ||
| 69 | + it "should be sent to comment notification recipients" do | ||
| 70 | + @email.to.should == recipients | ||
| 71 | + end | ||
| 72 | + | ||
| 73 | + it "should have the notices count in the body" do | ||
| 74 | + @email.should have_body_text("This err has occurred 2 times") | ||
| 75 | + end | ||
| 76 | + | ||
| 77 | + it "should have the comment body" do | ||
| 78 | + @email.should have_body_text(comment.body) | ||
| 79 | + end | ||
| 80 | + end | ||
| 81 | +end |
spec/models/app_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe App do | 3 | describe App do |
| 4 | + context "Attributes" do | ||
| 5 | + it { should have_field(:_id).of_type(String) } | ||
| 6 | + it { should have_field(:name).of_type(String) } | ||
| 7 | + it { should have_fields(:api_key, :github_repo, :bitbucket_repo, :asset_host, :repository_branch) } | ||
| 8 | + it { should have_fields(:resolve_errs_on_deploy, :notify_all_users, :notify_on_errs, :notify_on_deploys).of_type(Boolean) } | ||
| 9 | + it { should have_field(:email_at_notices).of_type(Array).with_default_value_of(Errbit::Config.email_at_notices) } | ||
| 10 | + end | ||
| 11 | + | ||
| 4 | context 'validations' do | 12 | context 'validations' do |
| 5 | it 'requires a name' do | 13 | it 'requires a name' do |
| 6 | app = Fabricate.build(:app, :name => nil) | 14 | app = Fabricate.build(:app, :name => nil) |
| @@ -96,7 +104,7 @@ describe App do | @@ -96,7 +104,7 @@ describe App do | ||
| 96 | context '#github_url_to_file' do | 104 | context '#github_url_to_file' do |
| 97 | it 'resolves to full path to file' do | 105 | it 'resolves to full path to file' do |
| 98 | app = Fabricate(:app, :github_repo => "errbit/errbit") | 106 | app = Fabricate(:app, :github_repo => "errbit/errbit") |
| 99 | - app.github_url_to_file('/path/to/file').should == "https://github.com/errbit/errbit/blob/master/path/to/file" | 107 | + app.github_url_to_file('path/to/file').should == "https://github.com/errbit/errbit/blob/master/path/to/file" |
| 100 | end | 108 | end |
| 101 | end | 109 | end |
| 102 | 110 | ||
| @@ -124,6 +132,25 @@ describe App do | @@ -124,6 +132,25 @@ describe App do | ||
| 124 | end | 132 | end |
| 125 | end | 133 | end |
| 126 | 134 | ||
| 135 | + context "emailable?" do | ||
| 136 | + it "should be true if notify on errs and there are notification recipients" do | ||
| 137 | + app = Fabricate(:app, :notify_on_errs => true, :notify_all_users => false) | ||
| 138 | + 2.times { Fabricate(:watcher, :app => app) } | ||
| 139 | + app.emailable?.should be_true | ||
| 140 | + end | ||
| 141 | + | ||
| 142 | + it "should be false if notify on errs is disabled" do | ||
| 143 | + app = Fabricate(:app, :notify_on_errs => false, :notify_all_users => false) | ||
| 144 | + 2.times { Fabricate(:watcher, :app => app) } | ||
| 145 | + app.emailable?.should be_false | ||
| 146 | + end | ||
| 147 | + | ||
| 148 | + it "should be false if there are no notification recipients" do | ||
| 149 | + app = Fabricate(:app, :notify_on_errs => true, :notify_all_users => false) | ||
| 150 | + app.watchers.should be_empty | ||
| 151 | + app.emailable?.should be_false | ||
| 152 | + end | ||
| 153 | + end | ||
| 127 | 154 | ||
| 128 | context "copying attributes from existing app" do | 155 | context "copying attributes from existing app" do |
| 129 | it "should only copy the necessary fields" do | 156 | it "should only copy the necessary fields" do |
| @@ -137,135 +164,58 @@ describe App do | @@ -137,135 +164,58 @@ describe App do | ||
| 137 | end | 164 | end |
| 138 | end | 165 | end |
| 139 | 166 | ||
| 140 | - | ||
| 141 | context '#find_or_create_err!' do | 167 | context '#find_or_create_err!' do |
| 142 | - before do | ||
| 143 | - @app = Fabricate(:app) | ||
| 144 | - @conditions = { | 168 | + let(:app) { Fabricate(:app) } |
| 169 | + let(:conditions) { { | ||
| 145 | :error_class => 'Whoops', | 170 | :error_class => 'Whoops', |
| 146 | - :component => 'Foo', | ||
| 147 | - :action => 'bar', | ||
| 148 | - :environment => 'production' | 171 | + :environment => 'production', |
| 172 | + :fingerprint => 'some-finger-print' | ||
| 149 | } | 173 | } |
| 150 | - end | 174 | + } |
| 151 | 175 | ||
| 152 | it 'returns the correct err if one already exists' do | 176 | it 'returns the correct err if one already exists' do |
| 153 | - existing = Fabricate(:err, @conditions.merge(:problem => Fabricate(:problem, :app => @app))) | ||
| 154 | - Err.where(@conditions).first.should == existing | ||
| 155 | - @app.find_or_create_err!(@conditions).should == existing | 177 | + existing = Fabricate(:err, conditions.merge(:problem => Fabricate(:problem, :app => app))) |
| 178 | + Err.where(conditions).first.should == existing | ||
| 179 | + app.find_or_create_err!(conditions).should == existing | ||
| 156 | end | 180 | end |
| 157 | 181 | ||
| 158 | it 'assigns the returned err to the given app' do | 182 | it 'assigns the returned err to the given app' do |
| 159 | - @app.find_or_create_err!(@conditions).app.should == @app | 183 | + app.find_or_create_err!(conditions).app.should == app |
| 160 | end | 184 | end |
| 161 | 185 | ||
| 162 | it 'creates a new problem if a matching one does not already exist' do | 186 | it 'creates a new problem if a matching one does not already exist' do |
| 163 | - Err.where(@conditions).first.should be_nil | 187 | + Err.where(conditions).first.should be_nil |
| 164 | lambda { | 188 | lambda { |
| 165 | - @app.find_or_create_err!(@conditions) | 189 | + app.find_or_create_err!(conditions) |
| 166 | }.should change(Problem,:count).by(1) | 190 | }.should change(Problem,:count).by(1) |
| 167 | end | 191 | end |
| 168 | - end | ||
| 169 | - | ||
| 170 | - | ||
| 171 | - context '#report_error!' do | ||
| 172 | - before do | ||
| 173 | - @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read | ||
| 174 | - @app = Fabricate(:app, :api_key => 'APIKEY') | ||
| 175 | - ErrorReport.any_instance.stub(:fingerprint).and_return('fingerprintdigest') | ||
| 176 | - end | ||
| 177 | - | ||
| 178 | - it 'finds the correct app' do | ||
| 179 | - @notice = App.report_error!(@xml) | ||
| 180 | - @notice.err.app.should == @app | ||
| 181 | - end | ||
| 182 | - | ||
| 183 | - it 'finds the correct err for the notice' do | ||
| 184 | - App.should_receive(:find_by_api_key!).and_return(@app) | ||
| 185 | - @app.should_receive(:find_or_create_err!).with({ | ||
| 186 | - :error_class => 'HoptoadTestingException', | ||
| 187 | - :component => 'application', | ||
| 188 | - :action => 'verify', | ||
| 189 | - :environment => 'development', | ||
| 190 | - :fingerprint => 'fingerprintdigest' | ||
| 191 | - }).and_return(err = Fabricate(:err)) | ||
| 192 | - err.notices.stub(:create!) | ||
| 193 | - @notice = App.report_error!(@xml) | ||
| 194 | - end | ||
| 195 | - | ||
| 196 | - it 'marks the err as unresolved if it was previously resolved' do | ||
| 197 | - App.should_receive(:find_by_api_key!).and_return(@app) | ||
| 198 | - @app.should_receive(:find_or_create_err!).with({ | ||
| 199 | - :error_class => 'HoptoadTestingException', | ||
| 200 | - :component => 'application', | ||
| 201 | - :action => 'verify', | ||
| 202 | - :environment => 'development', | ||
| 203 | - :fingerprint => 'fingerprintdigest' | ||
| 204 | - }).and_return(err = Fabricate(:err, :problem => Fabricate(:problem, :resolved => true))) | ||
| 205 | - err.should be_resolved | ||
| 206 | - @notice = App.report_error!(@xml) | ||
| 207 | - @notice.err.should == err | ||
| 208 | - @notice.err.should_not be_resolved | ||
| 209 | - end | ||
| 210 | 192 | ||
| 211 | - it 'should create a new notice' do | ||
| 212 | - @notice = App.report_error!(@xml) | ||
| 213 | - @notice.should be_persisted | ||
| 214 | - end | ||
| 215 | - | ||
| 216 | - it 'assigns an err to the notice' do | ||
| 217 | - @notice = App.report_error!(@xml) | ||
| 218 | - @notice.err.should be_a(Err) | ||
| 219 | - end | ||
| 220 | - | ||
| 221 | - it 'captures the err message' do | ||
| 222 | - @notice = App.report_error!(@xml) | ||
| 223 | - @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.' | ||
| 224 | - end | ||
| 225 | - | ||
| 226 | - it 'captures the backtrace' do | ||
| 227 | - @notice = App.report_error!(@xml) | ||
| 228 | - @notice.backtrace_lines.size.should == 73 | ||
| 229 | - @notice.backtrace_lines.last['file'].should == '[GEM_ROOT]/bin/rake' | ||
| 230 | - end | ||
| 231 | - | ||
| 232 | - it 'captures the server_environment' do | ||
| 233 | - @notice = App.report_error!(@xml) | ||
| 234 | - @notice.server_environment['environment-name'].should == 'development' | ||
| 235 | - end | ||
| 236 | - | ||
| 237 | - it 'captures the request' do | ||
| 238 | - @notice = App.report_error!(@xml) | ||
| 239 | - @notice.request['url'].should == 'http://example.org/verify' | ||
| 240 | - @notice.request['params']['controller'].should == 'application' | ||
| 241 | - end | ||
| 242 | - | ||
| 243 | - it 'captures the notifier' do | ||
| 244 | - @notice = App.report_error!(@xml) | ||
| 245 | - @notice.notifier['name'].should == 'Hoptoad Notifier' | ||
| 246 | - end | ||
| 247 | - | ||
| 248 | - it "should handle params without 'request' section" do | ||
| 249 | - xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read | ||
| 250 | - lambda { App.report_error!(xml) }.should_not raise_error | 193 | + context "without error_class" do |
| 194 | + let(:conditions) { { | ||
| 195 | + :environment => 'production', | ||
| 196 | + :fingerprint => 'some-finger-print' | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | + it 'save the err' do | ||
| 200 | + Err.where(conditions).first.should be_nil | ||
| 201 | + lambda { | ||
| 202 | + app.find_or_create_err!(conditions) | ||
| 203 | + }.should change(Problem,:count).by(1) | ||
| 204 | + end | ||
| 251 | end | 205 | end |
| 206 | + end | ||
| 252 | 207 | ||
| 253 | - it "should handle params with only a single line of backtrace" do | ||
| 254 | - xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read | ||
| 255 | - lambda { @notice = App.report_error!(xml) }.should_not raise_error | ||
| 256 | - @notice.backtrace_lines.length.should == 1 | 208 | + describe ".find_by_api_key!" do |
| 209 | + it 'return the app with api_key' do | ||
| 210 | + app = Fabricate(:app) | ||
| 211 | + expect(App.find_by_api_key!(app.api_key)).to eq app | ||
| 257 | end | 212 | end |
| 258 | - | ||
| 259 | - it 'captures the current_user' do | ||
| 260 | - @notice = App.report_error!(@xml) | ||
| 261 | - @notice.current_user['id'].should == '123' | ||
| 262 | - @notice.current_user['name'].should == 'Mr. Bean' | ||
| 263 | - @notice.current_user['email'].should == 'mr.bean@example.com' | ||
| 264 | - @notice.current_user['username'].should == 'mrbean' | 213 | + it 'raise Mongoid::Errors::DocumentNotFound if not found' do |
| 214 | + expect { | ||
| 215 | + App.find_by_api_key!('foo') | ||
| 216 | + }.to raise_error(Mongoid::Errors::DocumentNotFound) | ||
| 265 | end | 217 | end |
| 266 | - | ||
| 267 | end | 218 | end |
| 268 | 219 | ||
| 269 | - | ||
| 270 | end | 220 | end |
| 271 | 221 |
spec/models/backtrace_line_normalizer_spec.rb
| @@ -3,12 +3,25 @@ require 'spec_helper' | @@ -3,12 +3,25 @@ require 'spec_helper' | ||
| 3 | describe BacktraceLineNormalizer do | 3 | describe BacktraceLineNormalizer do |
| 4 | subject { described_class.new(raw_line).call } | 4 | subject { described_class.new(raw_line).call } |
| 5 | 5 | ||
| 6 | - describe "sanitize file" do | ||
| 7 | - let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => ActiveSupport.methods.shuffle.first.to_s } } | 6 | + describe "sanitize" do |
| 7 | + context "unknown file and method" do | ||
| 8 | + let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => nil } } | ||
| 8 | 9 | ||
| 9 | - it "should replace nil file with [unknown source]" do | ||
| 10 | - subject['file'].should == "[unknown source]" | 10 | + it "should replace nil file with [unknown source]" do |
| 11 | + subject['file'].should == "[unknown source]" | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + it "should replace nil method with [unknown method]" do | ||
| 15 | + subject['method'].should == "[unknown method]" | ||
| 16 | + end | ||
| 11 | end | 17 | end |
| 12 | 18 | ||
| 19 | + context "in app file" do | ||
| 20 | + let(:raw_line) { { 'number' => rand(999), 'file' => "[PROJECT_ROOT]/assets/file.js?body=1", 'method' => nil } } | ||
| 21 | + | ||
| 22 | + it "should strip query strings from files" do | ||
| 23 | + subject['file'].should == "[PROJECT_ROOT]/assets/file.js" | ||
| 24 | + end | ||
| 25 | + end | ||
| 13 | end | 26 | end |
| 14 | end | 27 | end |
| @@ -0,0 +1,12 @@ | @@ -0,0 +1,12 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe BacktraceLine do | ||
| 4 | + subject { described_class.new(raw_line) } | ||
| 5 | + | ||
| 6 | + describe "root at the start of decorated filename" do | ||
| 7 | + let(:raw_line) { { 'number' => rand(999), 'file' => '[PROJECT_ROOT]/app/controllers/pages_controller.rb', 'method' => ActiveSupport.methods.shuffle.first.to_s } } | ||
| 8 | + it "should leave leading root symbol in filepath" do | ||
| 9 | + subject.decorated_path.should == 'app/controllers/' | ||
| 10 | + end | ||
| 11 | + end | ||
| 12 | +end |
spec/models/backtrace_spec.rb
| @@ -22,8 +22,8 @@ describe Backtrace do | @@ -22,8 +22,8 @@ describe Backtrace do | ||
| 22 | 22 | ||
| 23 | describe "find_or_create" do | 23 | describe "find_or_create" do |
| 24 | subject { described_class.find_or_create(attributes) } | 24 | subject { described_class.find_or_create(attributes) } |
| 25 | - let(:attributes) { mock :attributes } | ||
| 26 | - let(:backtrace) { mock :backtrace } | 25 | + let(:attributes) { double :attributes } |
| 26 | + let(:backtrace) { double :backtrace } | ||
| 27 | 27 | ||
| 28 | before { described_class.stub(:new => backtrace) } | 28 | before { described_class.stub(:new => backtrace) } |
| 29 | 29 | ||
| @@ -37,7 +37,7 @@ describe Backtrace do | @@ -37,7 +37,7 @@ describe Backtrace do | ||
| 37 | end | 37 | end |
| 38 | 38 | ||
| 39 | context "similar backtrace exist" do | 39 | context "similar backtrace exist" do |
| 40 | - let(:similar_backtrace) { mock :similar_backtrace } | 40 | + let(:similar_backtrace) { double :similar_backtrace } |
| 41 | before { backtrace.stub(:similar => similar_backtrace) } | 41 | before { backtrace.stub(:similar => similar_backtrace) } |
| 42 | 42 | ||
| 43 | it { should == similar_backtrace } | 43 | it { should == similar_backtrace } |
| @@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe CommentObserver do | ||
| 4 | + context 'when a Comment is saved' do | ||
| 5 | + let(:comment) { Fabricate.build(:comment) } | ||
| 6 | + | ||
| 7 | + context 'and it is emailable?' do | ||
| 8 | + before { comment.stub(:emailable?).and_return(true) } | ||
| 9 | + | ||
| 10 | + it 'should send an email notification' do | ||
| 11 | + Mailer.should_receive(:comment_notification). | ||
| 12 | + with(comment). | ||
| 13 | + and_return(double('email', :deliver => true)) | ||
| 14 | + comment.save | ||
| 15 | + end | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + context 'and it is not emailable?' do | ||
| 19 | + before { comment.stub(:emailable?).and_return(false) } | ||
| 20 | + | ||
| 21 | + it 'should not send an email notification' do | ||
| 22 | + Mailer.should_not_receive(:comment_notification) | ||
| 23 | + comment.save | ||
| 24 | + end | ||
| 25 | + end | ||
| 26 | + end | ||
| 27 | +end |
spec/models/comment_spec.rb
| @@ -8,5 +8,48 @@ describe Comment do | @@ -8,5 +8,48 @@ describe Comment do | ||
| 8 | comment.errors[:body].should include("can't be blank") | 8 | comment.errors[:body].should include("can't be blank") |
| 9 | end | 9 | end |
| 10 | end | 10 | end |
| 11 | -end | ||
| 12 | 11 | ||
| 12 | + context 'notification_recipients' do | ||
| 13 | + let(:app) { Fabricate(:app) } | ||
| 14 | + let!(:watcher) { Fabricate(:watcher, :app => app) } | ||
| 15 | + let(:err) { Fabricate(:problem, :app => app) } | ||
| 16 | + let(:comment_user) { Fabricate(:user, :email => 'author@example.com') } | ||
| 17 | + let(:comment) { Fabricate.build(:comment, :err => err, :user => comment_user) } | ||
| 18 | + | ||
| 19 | + before do | ||
| 20 | + Fabricate(:user_watcher, :app => app, :user => comment_user) | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + it 'includes app notification_recipients except user email' do | ||
| 24 | + comment.notification_recipients.should == [watcher.address] | ||
| 25 | + end | ||
| 26 | + end | ||
| 27 | + | ||
| 28 | + context 'emailable?' do | ||
| 29 | + let(:app) { Fabricate(:app, :notify_on_errs => true) } | ||
| 30 | + let!(:watcher) { Fabricate(:watcher, :app => app) } | ||
| 31 | + let(:err) { Fabricate(:problem, :app => app) } | ||
| 32 | + let(:comment_user) { Fabricate(:user, :email => 'author@example.com') } | ||
| 33 | + let(:comment) { Fabricate.build(:comment, :err => err, :user => comment_user) } | ||
| 34 | + | ||
| 35 | + before do | ||
| 36 | + Fabricate(:user_watcher, :app => app, :user => comment_user) | ||
| 37 | + end | ||
| 38 | + | ||
| 39 | + it 'should be true if app is emailable? and there are notification recipients' do | ||
| 40 | + comment.emailable?.should be_true | ||
| 41 | + end | ||
| 42 | + | ||
| 43 | + it 'should be false if app is not emailable?' do | ||
| 44 | + app.update_attribute(:notify_on_errs, false) | ||
| 45 | + comment.notification_recipients.should be_any | ||
| 46 | + comment.emailable?.should be_false | ||
| 47 | + end | ||
| 48 | + | ||
| 49 | + it 'should be false if there are no notification recipients' do | ||
| 50 | + watcher.destroy | ||
| 51 | + app.emailable?.should be_true | ||
| 52 | + comment.emailable?.should be_false | ||
| 53 | + end | ||
| 54 | + end | ||
| 55 | +end |
spec/models/deploy_observer_spec.rb
| @@ -5,7 +5,7 @@ describe DeployObserver do | @@ -5,7 +5,7 @@ describe DeployObserver do | ||
| 5 | context 'and the app should notify on deploys' do | 5 | context 'and the app should notify on deploys' do |
| 6 | it 'should send an email notification' do | 6 | it 'should send an email notification' do |
| 7 | Mailer.should_receive(:deploy_notification). | 7 | Mailer.should_receive(:deploy_notification). |
| 8 | - and_return(mock('email', :deliver => true)) | 8 | + and_return(double('email', :deliver => true)) |
| 9 | Fabricate(:deploy, :app => Fabricate(:app_with_watcher, :notify_on_deploys => true)) | 9 | Fabricate(:deploy, :app => Fabricate(:app_with_watcher, :notify_on_deploys => true)) |
| 10 | end | 10 | end |
| 11 | end | 11 | end |
spec/models/err_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe Err do | 3 | describe Err do |
| 4 | - it 'sets a default error_class and environment' do | ||
| 5 | - err = Err.new | ||
| 6 | - err.error_class.should == "UnknownError" | ||
| 7 | - err.environment.should == "unknown" | 4 | + |
| 5 | + context 'validations' do | ||
| 6 | + it 'requires a fingerprint' do | ||
| 7 | + err = Fabricate.build(:err, :fingerprint => nil) | ||
| 8 | + err.should_not be_valid | ||
| 9 | + err.errors[:fingerprint].should include("can't be blank") | ||
| 10 | + end | ||
| 11 | + | ||
| 12 | + it 'requires a problem' do | ||
| 13 | + err = Fabricate.build(:err, :problem_id => nil) | ||
| 14 | + err.should_not be_valid | ||
| 15 | + err.errors[:problem_id].should include("can't be blank") | ||
| 16 | + end | ||
| 8 | end | 17 | end |
| 9 | -end | ||
| 10 | 18 | ||
| 19 | +end |
| @@ -0,0 +1,233 @@ | @@ -0,0 +1,233 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | +require 'airbrake/version' | ||
| 3 | +require 'airbrake/backtrace' | ||
| 4 | +require 'airbrake/notice' | ||
| 5 | + | ||
| 6 | +# MonkeyPatch to instanciate a Airbrake::Notice without configure | ||
| 7 | +# Airbrake | ||
| 8 | +# | ||
| 9 | +module Airbrake | ||
| 10 | + API_VERSION = '2.4' | ||
| 11 | + | ||
| 12 | + class Notice | ||
| 13 | + def framework | ||
| 14 | + 'rails' | ||
| 15 | + end | ||
| 16 | + end | ||
| 17 | +end | ||
| 18 | + | ||
| 19 | +describe ErrorReport do | ||
| 20 | + context "with notice without line of backtrace" do | ||
| 21 | + let(:xml){ | ||
| 22 | + Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + let(:error_report) { | ||
| 26 | + ErrorReport.new(xml) | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + let!(:app) { | ||
| 30 | + Fabricate( | ||
| 31 | + :app, | ||
| 32 | + :api_key => 'APIKEY' | ||
| 33 | + ) | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + describe "#app" do | ||
| 37 | + it 'find the good app' do | ||
| 38 | + expect(error_report.app).to eq app | ||
| 39 | + end | ||
| 40 | + end | ||
| 41 | + | ||
| 42 | + describe "#backtrace" do | ||
| 43 | + | ||
| 44 | + it 'should have valid backtrace' do | ||
| 45 | + error_report.backtrace.should be_valid | ||
| 46 | + end | ||
| 47 | + end | ||
| 48 | + | ||
| 49 | + describe "#generate_notice!" do | ||
| 50 | + it "save a notice" do | ||
| 51 | + expect { | ||
| 52 | + error_report.generate_notice! | ||
| 53 | + }.to change { | ||
| 54 | + app.reload.problems.count | ||
| 55 | + }.by(1) | ||
| 56 | + end | ||
| 57 | + context "with notice generate by Airbrake gem" do | ||
| 58 | + let(:xml) { Airbrake::Notice.new( | ||
| 59 | + :exception => Exception.new, | ||
| 60 | + :api_key => 'APIKEY', | ||
| 61 | + :project_root => Rails.root | ||
| 62 | + ).to_xml } | ||
| 63 | + it 'save a notice' do | ||
| 64 | + expect { | ||
| 65 | + error_report.generate_notice! | ||
| 66 | + }.to change { | ||
| 67 | + app.reload.problems.count | ||
| 68 | + }.by(1) | ||
| 69 | + end | ||
| 70 | + end | ||
| 71 | + | ||
| 72 | + describe "notice create" do | ||
| 73 | + before { error_report.generate_notice! } | ||
| 74 | + subject { error_report.notice } | ||
| 75 | + its(:message) { 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.' } | ||
| 76 | + its(:framework) { should == 'Rails: 3.2.11' } | ||
| 77 | + | ||
| 78 | + it 'has complete backtrace' do | ||
| 79 | + subject.backtrace_lines.size.should == 73 | ||
| 80 | + subject.backtrace_lines.last['file'].should == '[GEM_ROOT]/bin/rake' | ||
| 81 | + end | ||
| 82 | + it 'has server_environement' do | ||
| 83 | + subject.server_environment['environment-name'].should == 'development' | ||
| 84 | + end | ||
| 85 | + | ||
| 86 | + it 'has request' do | ||
| 87 | + subject.request['url'].should == 'http://example.org/verify' | ||
| 88 | + subject.request['params']['controller'].should == 'application' | ||
| 89 | + end | ||
| 90 | + | ||
| 91 | + it 'has notifier' do | ||
| 92 | + subject.notifier['name'].should == 'Hoptoad Notifier' | ||
| 93 | + end | ||
| 94 | + | ||
| 95 | + it 'get user_attributes' do | ||
| 96 | + subject.user_attributes['id'].should == '123' | ||
| 97 | + subject.user_attributes['name'].should == 'Mr. Bean' | ||
| 98 | + subject.user_attributes['email'].should == 'mr.bean@example.com' | ||
| 99 | + subject.user_attributes['username'].should == 'mrbean' | ||
| 100 | + end | ||
| 101 | + it 'valid env_vars' do | ||
| 102 | + # XML: <var key="SCRIPT_NAME"/> | ||
| 103 | + subject.env_vars.should have_key('SCRIPT_NAME') | ||
| 104 | + subject.env_vars['SCRIPT_NAME'].should be_nil # blank ends up nil | ||
| 105 | + | ||
| 106 | + # XML representation: | ||
| 107 | + # <var key="rack.session.options"> | ||
| 108 | + # <var key="secure">false</var> | ||
| 109 | + # <var key="httponly">true</var> | ||
| 110 | + # <var key="path">/</var> | ||
| 111 | + # <var key="expire_after"/> | ||
| 112 | + # <var key="domain"/> | ||
| 113 | + # <var key="id"/> | ||
| 114 | + # </var> | ||
| 115 | + expected = { | ||
| 116 | + 'secure' => 'false', | ||
| 117 | + 'httponly' => 'true', | ||
| 118 | + 'path' => '/', | ||
| 119 | + 'expire_after' => nil, | ||
| 120 | + 'domain' => nil, | ||
| 121 | + 'id' => nil | ||
| 122 | + } | ||
| 123 | + subject.env_vars.should have_key('rack_session_options') | ||
| 124 | + subject.env_vars['rack_session_options'].should eql(expected) | ||
| 125 | + end | ||
| 126 | + end | ||
| 127 | + | ||
| 128 | + it 'save a notice assignes to err' do | ||
| 129 | + error_report.generate_notice! | ||
| 130 | + error_report.notice.err.should be_a(Err) | ||
| 131 | + end | ||
| 132 | + | ||
| 133 | + it 'memoize the notice' do | ||
| 134 | + expect { | ||
| 135 | + error_report.generate_notice! | ||
| 136 | + error_report.generate_notice! | ||
| 137 | + }.to change { | ||
| 138 | + Notice.count | ||
| 139 | + }.by(1) | ||
| 140 | + end | ||
| 141 | + | ||
| 142 | + it 'find the correct err for the notice' do | ||
| 143 | + err = Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)) | ||
| 144 | + | ||
| 145 | + ErrorReport.any_instance.stub(:fingerprint).and_return(err.fingerprint) | ||
| 146 | + | ||
| 147 | + expect { | ||
| 148 | + error_report.generate_notice! | ||
| 149 | + }.to change { | ||
| 150 | + error_report.error.resolved? | ||
| 151 | + }.from(true).to(false) | ||
| 152 | + end | ||
| 153 | + | ||
| 154 | + context "with notification service configured" do | ||
| 155 | + before do | ||
| 156 | + app.notify_on_errs = true | ||
| 157 | + app.watchers.build(:email => 'foo@example.com') | ||
| 158 | + app.save | ||
| 159 | + end | ||
| 160 | + it 'send email' do | ||
| 161 | + notice = error_report.generate_notice! | ||
| 162 | + email = ActionMailer::Base.deliveries.last | ||
| 163 | + email.to.should include(app.watchers.first.email) | ||
| 164 | + email.subject.should include(notice.message.truncate(50)) | ||
| 165 | + email.subject.should include("[#{app.name}]") | ||
| 166 | + email.subject.should include("[#{notice.environment_name}]") | ||
| 167 | + end | ||
| 168 | + end | ||
| 169 | + | ||
| 170 | + context "with xml without request section" do | ||
| 171 | + let(:xml){ | ||
| 172 | + Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read | ||
| 173 | + } | ||
| 174 | + it "save a notice" do | ||
| 175 | + expect { | ||
| 176 | + error_report.generate_notice! | ||
| 177 | + }.to change { | ||
| 178 | + app.reload.problems.count | ||
| 179 | + }.by(1) | ||
| 180 | + end | ||
| 181 | + end | ||
| 182 | + | ||
| 183 | + context "with xml with only a single line of backtrace" do | ||
| 184 | + let(:xml){ | ||
| 185 | + Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read | ||
| 186 | + } | ||
| 187 | + it "save a notice" do | ||
| 188 | + expect { | ||
| 189 | + error_report.generate_notice! | ||
| 190 | + }.to change { | ||
| 191 | + app.reload.problems.count | ||
| 192 | + }.by(1) | ||
| 193 | + end | ||
| 194 | + end | ||
| 195 | + end | ||
| 196 | + | ||
| 197 | + describe "#valid?" do | ||
| 198 | + context "with valid error report" do | ||
| 199 | + it "return true" do | ||
| 200 | + expect(error_report.valid?).to be true | ||
| 201 | + end | ||
| 202 | + end | ||
| 203 | + context "with not valid api_key" do | ||
| 204 | + before do | ||
| 205 | + App.where(:api_key => app.api_key).delete_all | ||
| 206 | + end | ||
| 207 | + it "return false" do | ||
| 208 | + expect(error_report.valid?).to be false | ||
| 209 | + end | ||
| 210 | + end | ||
| 211 | + end | ||
| 212 | + | ||
| 213 | + describe "#notice" do | ||
| 214 | + context "before generate_notice!" do | ||
| 215 | + it 'return nil' do | ||
| 216 | + expect(error_report.notice).to be nil | ||
| 217 | + end | ||
| 218 | + end | ||
| 219 | + | ||
| 220 | + context "after generate_notice!" do | ||
| 221 | + before do | ||
| 222 | + error_report.generate_notice! | ||
| 223 | + end | ||
| 224 | + | ||
| 225 | + it 'return the notice' do | ||
| 226 | + expect(error_report.notice).to be_a Notice | ||
| 227 | + end | ||
| 228 | + | ||
| 229 | + end | ||
| 230 | + end | ||
| 231 | + | ||
| 232 | + end | ||
| 233 | +end |
| @@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +Fabrication::Config.fabricator_dir.each do |folder| | ||
| 4 | + Dir.glob(File.join(Rails.root, folder, '**', '*.rb')).each do |file| | ||
| 5 | + require file | ||
| 6 | + end | ||
| 7 | +end | ||
| 8 | + | ||
| 9 | +describe "Fabrication" do | ||
| 10 | + #TODO : when 1.8.7 drop support se directly Symbol#sort | ||
| 11 | + Fabrication::Fabricator.schematics.keys.sort_by(&:to_s).each do |fabricator_name| | ||
| 12 | + context "Fabricate(:#{fabricator_name})" do | ||
| 13 | + subject { Fabricate.build(fabricator_name) } | ||
| 14 | + | ||
| 15 | + it { should be_valid } | ||
| 16 | + end | ||
| 17 | + end | ||
| 18 | +end |
| @@ -0,0 +1,23 @@ | @@ -0,0 +1,23 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe Fingerprint do | ||
| 4 | + | ||
| 5 | + context '#generate' do | ||
| 6 | + before do | ||
| 7 | + @backtrace = Backtrace.find_or_create(:raw => [ | ||
| 8 | + {"number"=>"425", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"_run__2115867319__process_action__262109504__callbacks"}, | ||
| 9 | + {"number"=>"404", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"send"}, | ||
| 10 | + {"number"=>"404", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"_run_process_action_callbacks"} | ||
| 11 | + ]) | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + it 'should create the same fingerprint for two notices with the same backtrace' do | ||
| 15 | + notice1 = Fabricate.build(:notice, :backtrace => @backtrace) | ||
| 16 | + notice2 = Fabricate.build(:notice, :backtrace => @backtrace) | ||
| 17 | + | ||
| 18 | + Fingerprint.generate(notice1, "api key").should == Fingerprint.generate(notice2, "api key") | ||
| 19 | + end | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | +end | ||
| 23 | + |
spec/models/issue_trackers/bitbucket_issues_tracker_spec.rb
| @@ -26,16 +26,15 @@ describe IssueTrackers::BitbucketIssuesTracker do | @@ -26,16 +26,15 @@ describe IssueTrackers::BitbucketIssuesTracker do | ||
| 26 | } | 26 | } |
| 27 | EOF | 27 | EOF |
| 28 | 28 | ||
| 29 | - stub_request(:post, "https://#{tracker.api_token}:#{tracker.project_id}@bitbucket.org/api/1.0/repositories/test_username/test_repo/issues/").to_return(:status => 200, :headers => {}, :body => body ) | 29 | + stub_request(:post, "https://#{tracker.api_token}:#{tracker.project_id}@bitbucket.org/api/1.0/repositories/test_user/test_repo/issues/").to_return(:status => 200, :headers => {}, :body => body ) |
| 30 | 30 | ||
| 31 | problem.app.issue_tracker.create_issue(problem) | 31 | problem.app.issue_tracker.create_issue(problem) |
| 32 | problem.reload | 32 | problem.reload |
| 33 | 33 | ||
| 34 | - requested = have_requested(:post, "https://#{tracker.api_token}:#{tracker.project_id}@bitbucket.org/api/1.0/repositories/test_username/test_repo/issues/") | 34 | + requested = have_requested(:post, "https://#{tracker.api_token}:#{tracker.project_id}@bitbucket.org/api/1.0/repositories/test_user/test_repo/issues/") |
| 35 | WebMock.should requested.with(:title => /[production][foo#bar] FooError: Too Much Bar/) | 35 | WebMock.should requested.with(:title => /[production][foo#bar] FooError: Too Much Bar/) |
| 36 | WebMock.should requested.with(:content => /See this exception on Errbit/) | 36 | WebMock.should requested.with(:content => /See this exception on Errbit/) |
| 37 | 37 | ||
| 38 | problem.issue_link.should == @issue_link | 38 | problem.issue_link.should == @issue_link |
| 39 | end | 39 | end |
| 40 | end | 40 | end |
| 41 | - |
spec/models/issue_trackers/fogbugz_tracker_spec.rb
| @@ -9,7 +9,7 @@ describe IssueTrackers::FogbugzTracker do | @@ -9,7 +9,7 @@ describe IssueTrackers::FogbugzTracker do | ||
| 9 | number = 123 | 9 | number = 123 |
| 10 | @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}" | 10 | @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}" |
| 11 | response = "<response><token>12345</token><case><ixBug>123</ixBug></case></response>" | 11 | response = "<response><token>12345</token><case><ixBug>123</ixBug></case></response>" |
| 12 | - http_mock = mock | 12 | + http_mock = double |
| 13 | http_mock.should_receive(:new).and_return(http_mock) | 13 | http_mock.should_receive(:new).and_return(http_mock) |
| 14 | http_mock.should_receive(:request).twice.and_return(response) | 14 | http_mock.should_receive(:request).twice.and_return(response) |
| 15 | Fogbugz.adapter[:http] = http_mock | 15 | Fogbugz.adapter[:http] = http_mock |
spec/models/issue_trackers/github_issues_tracker_spec 2.rb
| @@ -1,30 +0,0 @@ | @@ -1,30 +0,0 @@ | ||
| 1 | -require 'spec_helper' | ||
| 2 | - | ||
| 3 | -describe IssueTrackers::GitlabTracker do | ||
| 4 | - it "should create an issue on Gitlab with problem params" do | ||
| 5 | - notice = Fabricate :notice | ||
| 6 | - tracker = Fabricate :gitlab_tracker, :app => notice.app | ||
| 7 | - problem = notice.problem | ||
| 8 | - | ||
| 9 | - number = 5 | ||
| 10 | - @issue_link = "#{tracker.account}/#{tracker.project_id}/issues/#{number}/#{tracker.api_token}" | ||
| 11 | - body = <<EOF | ||
| 12 | -{ | ||
| 13 | - "title": "Title" | ||
| 14 | -} | ||
| 15 | -EOF | ||
| 16 | - | ||
| 17 | - stub_request(:post, "#{tracker.account}/#{tracker.project_id}/issues/#{tracker.api_token}"). | ||
| 18 | - to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) | ||
| 19 | - | ||
| 20 | - problem.app.issue_tracker.create_issue(problem) | ||
| 21 | - problem.reload | ||
| 22 | - | ||
| 23 | - requested = have_requested(:post, "#{tracker.account}/#{tracker.project_id}/issues/#{tracker.api_token}") | ||
| 24 | - WebMock.should requested.with(:body => /[production][foo#bar] FooError: Too Much Bar/) | ||
| 25 | - WebMock.should requested.with(:body => /See this exception on Errbit/) | ||
| 26 | - | ||
| 27 | - problem.issue_link.should == @issue_link | ||
| 28 | - end | ||
| 29 | -end | ||
| 30 | - |
spec/models/issue_trackers/gitlab_issues_tracker_spec.rb
0 → 100644
| @@ -0,0 +1,32 @@ | @@ -0,0 +1,32 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe IssueTrackers::GitlabTracker do | ||
| 4 | + it "should create an issue on Gitlab with problem params" do | ||
| 5 | + notice = Fabricate :notice | ||
| 6 | + tracker = Fabricate :gitlab_tracker, :app => notice.app | ||
| 7 | + problem = notice.problem | ||
| 8 | + | ||
| 9 | + issue_id = 5 | ||
| 10 | + note_id = 10 | ||
| 11 | + issue_body = {:id => issue_id, :title => "[production][foo#bar] FooError: Too Much Bar", :description => "[See this exception on Errbit]"}.to_json | ||
| 12 | + note_body = {:id => note_id, :body => "Example note body"}.to_json | ||
| 13 | + | ||
| 14 | + stub_request(:post, "#{tracker.account}/api/v3/projects/#{tracker.project_id}/issues?private_token=#{tracker.api_token}"). | ||
| 15 | + with(:body => /.+/, :headers => {'Accept'=>'application/json'}). | ||
| 16 | + to_return(:status => 200, :body => issue_body, :headers => {'Accept'=>'application/json'}) | ||
| 17 | + | ||
| 18 | + stub_request(:post, "#{tracker.account}/api/v3/projects/#{tracker.project_id}/issues/#{issue_id}/notes?private_token=#{tracker.api_token}"). | ||
| 19 | + with(:body => /.+/, :headers => {'Accept'=>'application/json'}). | ||
| 20 | + to_return(:status => 200, :body => note_body, :headers => {'Accept'=>'application/json'}) | ||
| 21 | + | ||
| 22 | + problem.app.issue_tracker.create_issue(problem) | ||
| 23 | + problem.reload | ||
| 24 | + | ||
| 25 | + requested_issue = have_requested(:post, "#{tracker.account}/api/v3/projects/#{tracker.project_id}/issues?private_token=#{tracker.api_token}").with(:body => /.+/, :headers => {'Accept'=>'application/json'}) | ||
| 26 | + requested_note = have_requested(:post, "#{tracker.account}/api/v3/projects/#{tracker.project_id}/issues/#{issue_id}/notes?private_token=#{tracker.api_token}") | ||
| 27 | + WebMock.should requested_issue.with(:body => /%5Bproduction%5D%5Bfoo%23bar%5D%20FooError%3A%20Too%20Much%20Bar/) | ||
| 28 | + WebMock.should requested_issue.with(:body => /See%20this%20exception%20on%20Errbit/) | ||
| 29 | + | ||
| 30 | + end | ||
| 31 | +end | ||
| 32 | + |
spec/models/issue_trackers/redmine_tracker_spec.rb
| @@ -8,13 +8,17 @@ describe IssueTrackers::RedmineTracker do | @@ -8,13 +8,17 @@ describe IssueTrackers::RedmineTracker do | ||
| 8 | number = 5 | 8 | number = 5 |
| 9 | @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" | 9 | @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" |
| 10 | body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" | 10 | body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" |
| 11 | - stub_request(:post, "#{tracker.account}/issues.xml"). | 11 | + |
| 12 | + # Build base url with account URL, and username/password basic auth | ||
| 13 | + base_url = tracker.account.gsub 'http://', "http://#{tracker.username}:#{tracker.password}@" | ||
| 14 | + | ||
| 15 | + stub_request(:post, "#{base_url}/issues.xml"). | ||
| 12 | to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) | 16 | to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) |
| 13 | 17 | ||
| 14 | problem.app.issue_tracker.create_issue(problem) | 18 | problem.app.issue_tracker.create_issue(problem) |
| 15 | problem.reload | 19 | problem.reload |
| 16 | 20 | ||
| 17 | - requested = have_requested(:post, "#{tracker.account}/issues.xml") | 21 | + requested = have_requested(:post, "#{base_url}/issues.xml") |
| 18 | WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) | 22 | WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) |
| 19 | WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) | 23 | WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) |
| 20 | WebMock.should requested.with(:body => /<subject>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/subject>/) | 24 | WebMock.should requested.with(:body => /<subject>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/subject>/) |
| @@ -26,9 +30,9 @@ describe IssueTrackers::RedmineTracker do | @@ -26,9 +30,9 @@ describe IssueTrackers::RedmineTracker do | ||
| 26 | it "should generate a url where a file with line number can be viewed" do | 30 | it "should generate a url where a file with line number can be viewed" do |
| 27 | t = Fabricate(:redmine_tracker, :account => 'http://redmine.example.com', :project_id => "errbit") | 31 | t = Fabricate(:redmine_tracker, :account => 'http://redmine.example.com', :project_id => "errbit") |
| 28 | t.url_to_file("/example/file").should == | 32 | t.url_to_file("/example/file").should == |
| 29 | - 'http://redmine.example.com/projects/errbit/repository/annotate/example/file' | 33 | + 'http://redmine.example.com/projects/errbit/repository/revisions/master/changes/example/file' |
| 30 | t.url_to_file("/example/file", 25).should == | 34 | t.url_to_file("/example/file", 25).should == |
| 31 | - 'http://redmine.example.com/projects/errbit/repository/annotate/example/file#L25' | 35 | + 'http://redmine.example.com/projects/errbit/repository/revisions/master/changes/example/file#L25' |
| 32 | end | 36 | end |
| 33 | 37 | ||
| 34 | it "should use the alt_project_id to generate a file/linenumber url, if given" do | 38 | it "should use the alt_project_id to generate a file/linenumber url, if given" do |
| @@ -36,7 +40,6 @@ describe IssueTrackers::RedmineTracker do | @@ -36,7 +40,6 @@ describe IssueTrackers::RedmineTracker do | ||
| 36 | :project_id => "errbit", | 40 | :project_id => "errbit", |
| 37 | :alt_project_id => "actual_project") | 41 | :alt_project_id => "actual_project") |
| 38 | t.url_to_file("/example/file", 25).should == | 42 | t.url_to_file("/example/file", 25).should == |
| 39 | - 'http://redmine.example.com/projects/actual_project/repository/annotate/example/file#L25' | 43 | + 'http://redmine.example.com/projects/actual_project/repository/revisions/master/changes/example/file#L25' |
| 40 | end | 44 | end |
| 41 | end | 45 | end |
| 42 | - |
spec/models/issue_trackers/unfuddle_issues_tracker_spec.rb
0 → 100644
| @@ -0,0 +1,92 @@ | @@ -0,0 +1,92 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe IssueTrackers::UnfuddleTracker do | ||
| 4 | + | ||
| 5 | + let(:issue_link) { "https://test.unfuddle.com/projects/15/tickets/2436" } | ||
| 6 | + let(:notice) { Fabricate :notice } | ||
| 7 | + let(:tracker) { Fabricate :unfuddle_issues_tracker, :app => notice.app } | ||
| 8 | + let(:problem) { notice.problem } | ||
| 9 | + | ||
| 10 | + it "should create an issue on Unfuddle Issues with problem params, and set issue link for problem" do | ||
| 11 | +project_xml = <<EOF | ||
| 12 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 13 | +<project> | ||
| 14 | + <account-id type="integer">1</account-id> | ||
| 15 | + <archived type="boolean">false</archived> | ||
| 16 | + <assignee-on-resolve>reporter</assignee-on-resolve> | ||
| 17 | + <backup-frequency type="integer">0</backup-frequency> | ||
| 18 | + <close-ticket-simultaneously-default type="boolean">false</close-ticket-simultaneously-default> | ||
| 19 | + <default-ticket-report-id type="integer" nil="true"></default-ticket-report-id> | ||
| 20 | + <description nil="true"></description> | ||
| 21 | + <disk-usage type="integer">27932</disk-usage> | ||
| 22 | + <enable-time-tracking type="boolean">true</enable-time-tracking> | ||
| 23 | + <id type="integer">#{tracker.project_id}</id> | ||
| 24 | + <s3-access-key-id></s3-access-key-id> | ||
| 25 | + <s3-backup-enabled type="boolean">false</s3-backup-enabled> | ||
| 26 | + <s3-bucket-name></s3-bucket-name> | ||
| 27 | + <short-name>test-project</short-name> | ||
| 28 | + <theme>blue</theme> | ||
| 29 | + <ticket-field1-active type="boolean">false</ticket-field1-active> | ||
| 30 | + <ticket-field1-disposition>text</ticket-field1-disposition> | ||
| 31 | + <ticket-field1-title>Field 1</ticket-field1-title> | ||
| 32 | + <ticket-field2-active type="boolean">false</ticket-field2-active> | ||
| 33 | + <ticket-field2-disposition>text</ticket-field2-disposition> | ||
| 34 | + <ticket-field2-title>Field 2</ticket-field2-title> | ||
| 35 | + <ticket-field3-active type="boolean">false</ticket-field3-active> | ||
| 36 | + <ticket-field3-disposition>text</ticket-field3-disposition> | ||
| 37 | + <ticket-field3-title>Field 3</ticket-field3-title> | ||
| 38 | + <title>test-project</title> | ||
| 39 | + <created-at>2011-04-25T09:21:43Z</created-at> | ||
| 40 | + <updated-at>2013-03-08T08:03:02Z</updated-at> | ||
| 41 | +</project> | ||
| 42 | +EOF | ||
| 43 | + | ||
| 44 | + ticket_xml =<<EOF | ||
| 45 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 46 | +<ticket> | ||
| 47 | + <assignee-id type="integer">40</assignee-id> | ||
| 48 | + <component-id type="integer" nil="true"></component-id> | ||
| 49 | + <description nil="true"></description> | ||
| 50 | + <description-format>markdown</description-format> | ||
| 51 | + <due-on type="date" nil="true"></due-on> | ||
| 52 | + <field1-value-id type="integer" nil="true"></field1-value-id> | ||
| 53 | + <field2-value-id type="integer" nil="true"></field2-value-id> | ||
| 54 | + <field3-value-id type="integer" nil="true"></field3-value-id> | ||
| 55 | + <hours-estimate-current type="float">1268.7</hours-estimate-current> | ||
| 56 | + <hours-estimate-initial type="float">0.0</hours-estimate-initial> | ||
| 57 | + <id type="integer">2436</id> | ||
| 58 | + <milestone-id type="integer">78</milestone-id> | ||
| 59 | + <number type="integer">119</number> | ||
| 60 | + <priority>3</priority> | ||
| 61 | + <project-id type="integer">15</project-id> | ||
| 62 | + <reporter-id type="integer">40</reporter-id> | ||
| 63 | + <resolution></resolution> | ||
| 64 | + <resolution-description></resolution-description> | ||
| 65 | + <resolution-description-format>markdown</resolution-description-format> | ||
| 66 | + <severity-id type="integer" nil="true"></severity-id> | ||
| 67 | + <status>reopened</status> | ||
| 68 | + <summary>TEST-ticket.</summary> | ||
| 69 | + <version-id type="integer" nil="true"></version-id> | ||
| 70 | + <created-at>2012-06-27T17:49:06Z</created-at> | ||
| 71 | + <updated-at>2013-03-07T16:04:05Z</updated-at> | ||
| 72 | +</ticket> | ||
| 73 | +EOF | ||
| 74 | + | ||
| 75 | + stub_request(:get, "https://#{tracker.username}:#{tracker.password}@test.unfuddle.com/api/v1/projects/#{tracker.project_id}.xml"). | ||
| 76 | + to_return(:status => 200, :body => project_xml, :headers => {}) | ||
| 77 | + | ||
| 78 | + | ||
| 79 | + stub_request(:post, "https://#{tracker.username}:#{tracker.password}@test.unfuddle.com/api/v1/projects/#{tracker.project_id}/tickets.xml"). | ||
| 80 | + to_return(:status => 200, :body => ticket_xml, :headers => {}) | ||
| 81 | + | ||
| 82 | + problem.app.issue_tracker.create_issue(problem) | ||
| 83 | + problem.reload | ||
| 84 | + | ||
| 85 | + requested = have_requested(:post,"https://#{tracker.username}:#{tracker.password}@test.unfuddle.com/api/v1/projects/#{tracker.project_id}/tickets.xml" ) | ||
| 86 | + WebMock.should requested.with(:title => /[production][foo#bar] FooError: Too Much Bar/) | ||
| 87 | + WebMock.should requested.with(:content => /See this exception on Errbit/) | ||
| 88 | + | ||
| 89 | + problem.issue_link.should == issue_link | ||
| 90 | + problem.issue_type.should == IssueTrackers::UnfuddleTracker::Label | ||
| 91 | + end | ||
| 92 | +end |
spec/models/notice_observer_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | -describe NoticeObserver do | 3 | +describe "Callback on Notice" do |
| 4 | describe "email notifications (configured individually for each app)" do | 4 | describe "email notifications (configured individually for each app)" do |
| 5 | custom_thresholds = [2, 4, 8, 16, 32, 64] | 5 | custom_thresholds = [2, 4, 8, 16, 32, 64] |
| 6 | 6 | ||
| @@ -18,12 +18,13 @@ describe NoticeObserver do | @@ -18,12 +18,13 @@ describe NoticeObserver do | ||
| 18 | it "sends an email notification after #{threshold} notice(s)" do | 18 | it "sends an email notification after #{threshold} notice(s)" do |
| 19 | @err.problem.stub(:notices_count).and_return(threshold) | 19 | @err.problem.stub(:notices_count).and_return(threshold) |
| 20 | Mailer.should_receive(:err_notification). | 20 | Mailer.should_receive(:err_notification). |
| 21 | - and_return(mock('email', :deliver => true)) | 21 | + and_return(double('email', :deliver => true)) |
| 22 | Fabricate(:notice, :err => @err) | 22 | Fabricate(:notice, :err => @err) |
| 23 | end | 23 | end |
| 24 | end | 24 | end |
| 25 | end | 25 | end |
| 26 | 26 | ||
| 27 | + | ||
| 27 | describe "email notifications for a resolved issue" do | 28 | describe "email notifications for a resolved issue" do |
| 28 | before do | 29 | before do |
| 29 | Errbit::Config.per_app_email_at_notices = true | 30 | Errbit::Config.per_app_email_at_notices = true |
| @@ -38,12 +39,19 @@ describe NoticeObserver do | @@ -38,12 +39,19 @@ describe NoticeObserver do | ||
| 38 | it "should send email notification after 1 notice since an error has been resolved" do | 39 | it "should send email notification after 1 notice since an error has been resolved" do |
| 39 | @err.problem.resolve! | 40 | @err.problem.resolve! |
| 40 | Mailer.should_receive(:err_notification). | 41 | Mailer.should_receive(:err_notification). |
| 41 | - and_return(mock('email', :deliver => true)) | 42 | + and_return(double('email', :deliver => true)) |
| 43 | + Fabricate(:notice, :err => @err) | ||
| 44 | + end | ||
| 45 | + it 'self notify if mailer failed' do | ||
| 46 | + @err.problem.resolve! | ||
| 47 | + Mailer.should_receive(:err_notification). | ||
| 48 | + and_raise(ArgumentError) | ||
| 49 | + HoptoadNotifier.should_receive(:notify) | ||
| 42 | Fabricate(:notice, :err => @err) | 50 | Fabricate(:notice, :err => @err) |
| 43 | end | 51 | end |
| 44 | end | 52 | end |
| 45 | 53 | ||
| 46 | - describe "should send a notification if a notification service is configured" do | 54 | + describe "should send a notification if a notification service is configured with defaults" do |
| 47 | let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service))} | 55 | let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service))} |
| 48 | let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } | 56 | let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } |
| 49 | let(:backtrace) { Fabricate(:backtrace) } | 57 | let(:backtrace) { Fabricate(:backtrace) } |
| @@ -64,6 +72,30 @@ describe NoticeObserver do | @@ -64,6 +72,30 @@ describe NoticeObserver do | ||
| 64 | end | 72 | end |
| 65 | end | 73 | end |
| 66 | 74 | ||
| 75 | + describe "send a notification if a notification service is configured with defaults but failed" do | ||
| 76 | + let(:app) { Fabricate(:app_with_watcher, | ||
| 77 | + :notify_on_errs => true, | ||
| 78 | + :email_at_notices => [1, 100], :notification_service => Fabricate(:campfire_notification_service))} | ||
| 79 | + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 99)) } | ||
| 80 | + let(:backtrace) { Fabricate(:backtrace) } | ||
| 81 | + | ||
| 82 | + before do | ||
| 83 | + Errbit::Config.per_app_email_at_notices = true | ||
| 84 | + end | ||
| 85 | + | ||
| 86 | + after do | ||
| 87 | + Errbit::Config.per_app_email_at_notices = false | ||
| 88 | + end | ||
| 89 | + | ||
| 90 | + it "send email" do | ||
| 91 | + app.notification_service.should_receive(:create_notification).and_raise(ArgumentError) | ||
| 92 | + Mailer.should_receive(:err_notification).and_return(double(:deliver => true)) | ||
| 93 | + | ||
| 94 | + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'}, | ||
| 95 | + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | ||
| 96 | + end | ||
| 97 | + end | ||
| 98 | + | ||
| 67 | describe "should not send a notification if a notification service is not configured" do | 99 | describe "should not send a notification if a notification service is not configured" do |
| 68 | let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:notification_service))} | 100 | let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:notification_service))} |
| 69 | let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } | 101 | let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } |
| @@ -103,4 +135,41 @@ describe NoticeObserver do | @@ -103,4 +135,41 @@ describe NoticeObserver do | ||
| 103 | Fabricate(:notice, :err => err) | 135 | Fabricate(:notice, :err => err) |
| 104 | end | 136 | end |
| 105 | end | 137 | end |
| 138 | + | ||
| 139 | + describe "should send a notification at desired intervals" do | ||
| 140 | + let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service, :notify_at_notices => [1,2]))} | ||
| 141 | + let(:backtrace) { Fabricate(:backtrace) } | ||
| 142 | + | ||
| 143 | + before do | ||
| 144 | + Errbit::Config.per_app_email_at_notices = true | ||
| 145 | + end | ||
| 146 | + | ||
| 147 | + after do | ||
| 148 | + Errbit::Config.per_app_email_at_notices = false | ||
| 149 | + end | ||
| 150 | + | ||
| 151 | + it "should create a campfire notification on first notice" do | ||
| 152 | + err = Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 1)) | ||
| 153 | + app.notification_service.should_receive(:create_notification) | ||
| 154 | + | ||
| 155 | + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'}, | ||
| 156 | + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | ||
| 157 | + end | ||
| 158 | + | ||
| 159 | + it "should create a campfire notification on second notice" do | ||
| 160 | + err = Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 1)) | ||
| 161 | + app.notification_service.should_receive(:create_notification) | ||
| 162 | + | ||
| 163 | + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'}, | ||
| 164 | + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | ||
| 165 | + end | ||
| 166 | + | ||
| 167 | + it "should not create a campfire notification on third notice" do | ||
| 168 | + err = Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 1)) | ||
| 169 | + app.notification_service.should_receive(:create_notification) | ||
| 170 | + | ||
| 171 | + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'}, | ||
| 172 | + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) | ||
| 173 | + end | ||
| 174 | + end | ||
| 106 | end | 175 | end |
spec/models/notice_spec.rb
| @@ -37,7 +37,9 @@ describe Notice do | @@ -37,7 +37,9 @@ describe Notice do | ||
| 37 | 37 | ||
| 38 | describe "user agent" do | 38 | describe "user agent" do |
| 39 | it "should be parsed and human-readable" do | 39 | it "should be parsed and human-readable" do |
| 40 | - notice = Fabricate.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) | 40 | + notice = Fabricate.build(:notice, :request => {'cgi-data' => { |
| 41 | + 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16' | ||
| 42 | + }}) | ||
| 41 | notice.user_agent.browser.should == 'Chrome' | 43 | notice.user_agent.browser.should == 'Chrome' |
| 42 | notice.user_agent.version.to_s.should =~ /^10\.0/ | 44 | notice.user_agent.version.to_s.should =~ /^10\.0/ |
| 43 | end | 45 | end |
| @@ -51,7 +53,7 @@ describe Notice do | @@ -51,7 +53,7 @@ describe Notice do | ||
| 51 | describe "user agent string" do | 53 | describe "user agent string" do |
| 52 | it "should be parsed and human-readable" do | 54 | it "should be parsed and human-readable" do |
| 53 | notice = Fabricate.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) | 55 | notice = Fabricate.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) |
| 54 | - notice.user_agent_string.should == 'Chrome 10.0.648.204' | 56 | + notice.user_agent_string.should == 'Chrome 10.0.648.204 (OS X 10.6.7)' |
| 55 | end | 57 | end |
| 56 | 58 | ||
| 57 | it "should be nil if HTTP_USER_AGENT is blank" do | 59 | it "should be nil if HTTP_USER_AGENT is blank" do |
spec/models/notification_service/campfire_service_spec.rb
| @@ -8,7 +8,7 @@ describe NotificationService::CampfireService do | @@ -8,7 +8,7 @@ describe NotificationService::CampfireService do | ||
| 8 | problem = notice.problem | 8 | problem = notice.problem |
| 9 | 9 | ||
| 10 | #campy stubbing | 10 | #campy stubbing |
| 11 | - campy = mock('CampfireService') | 11 | + campy = double('CampfireService') |
| 12 | Campy::Room.stub(:new).and_return(campy) | 12 | Campy::Room.stub(:new).and_return(campy) |
| 13 | campy.stub(:speak) { true } | 13 | campy.stub(:speak) { true } |
| 14 | 14 |
spec/models/notification_service/flowdock_service_spec.rb
0 → 100644
| @@ -0,0 +1,17 @@ | @@ -0,0 +1,17 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe NotificationServices::FlowdockService do | ||
| 4 | + let(:service) { Fabricate.build(:flowdock_notification_service) } | ||
| 5 | + let(:app) { Fabricate(:app, :name => 'App #3') } | ||
| 6 | + let(:problem) { Fabricate(:problem, :app => app, :message => '<3') } | ||
| 7 | + | ||
| 8 | + it 'sends message in appropriate format' do | ||
| 9 | + Flowdock::Flow.any_instance.should_receive(:push_to_team_inbox) do |*args| | ||
| 10 | + args.first[:content].should_not include('<3') | ||
| 11 | + args.first[:content].should include('<3') | ||
| 12 | + | ||
| 13 | + args.first[:project].should eq('App3') | ||
| 14 | + end | ||
| 15 | + service.create_notification(problem) | ||
| 16 | + end | ||
| 17 | +end |
spec/models/notification_service/gtalk_service_spec.rb
| @@ -4,24 +4,122 @@ describe NotificationService::GtalkService do | @@ -4,24 +4,122 @@ describe NotificationService::GtalkService do | ||
| 4 | it "it should send a notification to gtalk" do | 4 | it "it should send a notification to gtalk" do |
| 5 | # setup | 5 | # setup |
| 6 | notice = Fabricate :notice | 6 | notice = Fabricate :notice |
| 7 | + problem = notice.problem | ||
| 7 | notification_service = Fabricate :gtalk_notification_service, :app => notice.app | 8 | notification_service = Fabricate :gtalk_notification_service, :app => notice.app |
| 8 | problem = notice.problem | 9 | problem = notice.problem |
| 9 | 10 | ||
| 10 | #gtalk stubbing | 11 | #gtalk stubbing |
| 11 | - gtalk = mock('GtalkService') | 12 | + gtalk = double('GtalkService') |
| 13 | + jid = double("jid") | ||
| 14 | + message = double("message") | ||
| 15 | + Jabber::JID.should_receive(:new).with(notification_service.subdomain).and_return(jid) | ||
| 16 | + Jabber::Client.should_receive(:new).with(jid).and_return(gtalk) | ||
| 17 | + gtalk.should_receive(:connect).with(notification_service.service) | ||
| 18 | + gtalk.should_receive(:auth).with(notification_service.api_token) | ||
| 19 | + message_value = """#{problem.app.name.to_s} | ||
| 20 | +http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} | ||
| 21 | +#{notification_service.notification_description problem}""" | ||
| 22 | + | ||
| 23 | + Jabber::Message.should_receive(:new).with(notification_service.user_id, message_value).and_return(message) | ||
| 24 | + Jabber::Message.should_receive(:new).with(notification_service.room_id, message_value).and_return(message) | ||
| 25 | + | ||
| 26 | + Jabber::MUC::SimpleMUCClient.should_receive(:new).and_return(gtalk) | ||
| 27 | + gtalk.should_receive(:join).with(notification_service.room_id + "/errbit") | ||
| 28 | + | ||
| 29 | + #assert | ||
| 30 | + gtalk.should_receive(:send).exactly(2).times.with(message) | ||
| 31 | + | ||
| 32 | + notification_service.create_notification(problem) | ||
| 33 | + end | ||
| 34 | + | ||
| 35 | + describe "multiple users_ids" do | ||
| 36 | + before(:each) do | ||
| 37 | + # setup | ||
| 38 | + @notice = Fabricate :notice | ||
| 39 | + @notification_service = Fabricate :gtalk_notification_service, :app => @notice.app | ||
| 40 | + @problem = @notice.problem | ||
| 41 | + @error_msg = """#{@problem.app.name.to_s} | ||
| 42 | +http://#{Errbit::Config.host}/apps/#{@problem.app.id.to_s} | ||
| 43 | +#{@notification_service.notification_description @problem}""" | ||
| 44 | + | ||
| 45 | + # gtalk stubbing | ||
| 46 | + @gtalk = double('GtalkService') | ||
| 47 | + @gtalk.should_receive(:connect) | ||
| 48 | + @gtalk.should_receive(:auth) | ||
| 49 | + jid = double("jid") | ||
| 50 | + Jabber::JID.stub(:new).with(@notification_service.subdomain).and_return(jid) | ||
| 51 | + Jabber::Client.stub(:new).with(jid).and_return(@gtalk) | ||
| 52 | + end | ||
| 53 | + it "should send a notification to all ',' separated users" do | ||
| 54 | + Jabber::Message.should_receive(:new).with("first@domain.org", @error_msg) | ||
| 55 | + Jabber::Message.should_receive(:new).with("second@domain.org", @error_msg) | ||
| 56 | + Jabber::Message.should_receive(:new).with("third@domain.org", @error_msg) | ||
| 57 | + Jabber::Message.should_receive(:new).with("fourth@domain.org", @error_msg) | ||
| 58 | + Jabber::MUC::SimpleMUCClient.should_not_receive(:new) | ||
| 59 | + @gtalk.should_receive(:send).exactly(4).times | ||
| 60 | + | ||
| 61 | + @notification_service.user_id = "first@domain.org,second@domain.org, third@domain.org , fourth@domain.org " | ||
| 62 | + @notification_service.room_id = "" | ||
| 63 | + @notification_service.create_notification(@problem) | ||
| 64 | + end | ||
| 65 | + it "should send a notification to all ';' separated users" do | ||
| 66 | + Jabber::Message.should_receive(:new).with("first@domain.org", @error_msg) | ||
| 67 | + Jabber::Message.should_receive(:new).with("second@domain.org", @error_msg) | ||
| 68 | + Jabber::Message.should_receive(:new).with("third@domain.org", @error_msg) | ||
| 69 | + Jabber::Message.should_receive(:new).with("fourth@domain.org", @error_msg) | ||
| 70 | + Jabber::MUC::SimpleMUCClient.should_not_receive(:new) | ||
| 71 | + @gtalk.should_receive(:send).exactly(4).times | ||
| 72 | + | ||
| 73 | + @notification_service.user_id = "first@domain.org;second@domain.org; third@domain.org ; fourth@domain.org " | ||
| 74 | + @notification_service.room_id = "" | ||
| 75 | + @notification_service.create_notification(@problem) | ||
| 76 | + end | ||
| 77 | + it "should send a notification to all ' ' separated users" do | ||
| 78 | + Jabber::Message.should_receive(:new).with("first@domain.org", @error_msg) | ||
| 79 | + Jabber::Message.should_receive(:new).with("second@domain.org", @error_msg) | ||
| 80 | + Jabber::Message.should_receive(:new).with("third@domain.org", @error_msg) | ||
| 81 | + Jabber::Message.should_receive(:new).with("fourth@domain.org", @error_msg) | ||
| 82 | + Jabber::MUC::SimpleMUCClient.should_not_receive(:new) | ||
| 83 | + @gtalk.should_receive(:send).exactly(4).times | ||
| 84 | + | ||
| 85 | + @notification_service.user_id = "first@domain.org second@domain.org third@domain.org fourth@domain.org " | ||
| 86 | + @notification_service.room_id = "" | ||
| 87 | + @notification_service.create_notification(@problem) | ||
| 88 | + end | ||
| 89 | + | ||
| 90 | + end | ||
| 91 | + | ||
| 92 | + it "it should send a notification to room only" do | ||
| 93 | + # setup | ||
| 94 | + notice = Fabricate :notice | ||
| 95 | + problem = notice.problem | ||
| 96 | + notification_service = Fabricate :gtalk_notification_service, :app => notice.app | ||
| 97 | + problem = notice.problem | ||
| 98 | + | ||
| 99 | + #gtalk stubbing | ||
| 100 | + gtalk = double('GtalkService') | ||
| 12 | jid = double("jid") | 101 | jid = double("jid") |
| 13 | message = double("message") | 102 | message = double("message") |
| 14 | Jabber::JID.should_receive(:new).with(notification_service.subdomain).and_return(jid) | 103 | Jabber::JID.should_receive(:new).with(notification_service.subdomain).and_return(jid) |
| 15 | Jabber::Client.should_receive(:new).with(jid).and_return(gtalk) | 104 | Jabber::Client.should_receive(:new).with(jid).and_return(gtalk) |
| 16 | gtalk.should_receive(:connect) | 105 | gtalk.should_receive(:connect) |
| 17 | gtalk.should_receive(:auth).with(notification_service.api_token) | 106 | gtalk.should_receive(:auth).with(notification_service.api_token) |
| 18 | - Jabber::Message.should_receive(:new).with(notification_service.room_id, "[errbit] http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} #{notification_service.notification_description problem}").and_return(message) | 107 | + message_value = """#{problem.app.name.to_s} |
| 108 | +http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} | ||
| 109 | +#{notification_service.notification_description problem}""" | ||
| 110 | + | ||
| 111 | + Jabber::Message.should_receive(:new).with(notification_service.room_id, message_value).and_return(message) | ||
| 112 | + | ||
| 113 | + Jabber::MUC::SimpleMUCClient.should_receive(:new).and_return(gtalk) | ||
| 114 | + gtalk.should_receive(:join).with(notification_service.room_id + "/errbit") | ||
| 115 | + | ||
| 116 | + notification_service.user_id = "" | ||
| 19 | 117 | ||
| 20 | #assert | 118 | #assert |
| 21 | gtalk.should_receive(:send).with(message) | 119 | gtalk.should_receive(:send).with(message) |
| 22 | 120 | ||
| 23 | - | ||
| 24 | notification_service.create_notification(problem) | 121 | notification_service.create_notification(problem) |
| 25 | end | 122 | end |
| 123 | + | ||
| 26 | end | 124 | end |
| 27 | 125 |
spec/models/notification_service/hipchat_service_spec.rb
| @@ -15,7 +15,7 @@ describe NotificationServices::HipchatService do | @@ -15,7 +15,7 @@ describe NotificationServices::HipchatService do | ||
| 15 | end | 15 | end |
| 16 | 16 | ||
| 17 | it 'escapes html in message' do | 17 | it 'escapes html in message' do |
| 18 | - service.stub(:notification_description => '<3') | 18 | + problem.stub(:message => '<3') |
| 19 | room.should_receive(:send) do |_, message| | 19 | room.should_receive(:send) do |_, message| |
| 20 | message.should_not include('<3') | 20 | message.should_not include('<3') |
| 21 | message.should include('<3') | 21 | message.should include('<3') |
spec/models/notification_service/hoiio_service_spec.rb
| @@ -8,7 +8,7 @@ describe NotificationService::HoiioService do | @@ -8,7 +8,7 @@ describe NotificationService::HoiioService do | ||
| 8 | problem = notice.problem | 8 | problem = notice.problem |
| 9 | 9 | ||
| 10 | # hoi stubbing | 10 | # hoi stubbing |
| 11 | - sms = mock('HoiioService') | 11 | + sms = double('HoiioService') |
| 12 | Hoi::SMS.stub(:new).and_return(sms) | 12 | Hoi::SMS.stub(:new).and_return(sms) |
| 13 | sms.stub(:send) { true } | 13 | sms.stub(:send) { true } |
| 14 | 14 |
| @@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe NotificationService::HubotService do | ||
| 4 | + it "it should send a notification to Hubot" do | ||
| 5 | + # setup | ||
| 6 | + notice = Fabricate :notice | ||
| 7 | + notification_service = Fabricate :hubot_notification_service, :app => notice.app | ||
| 8 | + problem = notice.problem | ||
| 9 | + | ||
| 10 | + # faraday stubbing | ||
| 11 | + HTTParty.should_receive(:post).with(notification_service.api_token, :body => {:message => an_instance_of(String), :room => notification_service.room_id}).and_return(true) | ||
| 12 | + | ||
| 13 | + notification_service.create_notification(problem) | ||
| 14 | + end | ||
| 15 | +end |
spec/models/notification_service/pushover_service_spec.rb
| @@ -8,7 +8,7 @@ describe NotificationService::PushoverService do | @@ -8,7 +8,7 @@ describe NotificationService::PushoverService do | ||
| 8 | problem = notice.problem | 8 | problem = notice.problem |
| 9 | 9 | ||
| 10 | # hoi stubbing | 10 | # hoi stubbing |
| 11 | - notification = mock('PushoverService') | 11 | + notification = double('PushoverService') |
| 12 | Rushover::Client.stub(:new).and_return(notification) | 12 | Rushover::Client.stub(:new).and_return(notification) |
| 13 | notification.stub(:notify) { true } | 13 | notification.stub(:notify) { true } |
| 14 | 14 |
spec/models/notification_service/webhook_service_spec.rb
0 → 100644
| @@ -0,0 +1,13 @@ | @@ -0,0 +1,13 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe NotificationService::WebhookService do | ||
| 4 | + it "it should send a notification to a user-specified URL" do | ||
| 5 | + notice = Fabricate :notice | ||
| 6 | + notification_service = Fabricate :webhook_notification_service, :app => notice.app | ||
| 7 | + problem = notice.problem | ||
| 8 | + | ||
| 9 | + HTTParty.should_receive(:post).with(notification_service.api_token, :body => {:problem => problem.to_json}).and_return(true) | ||
| 10 | + | ||
| 11 | + notification_service.create_notification(problem) | ||
| 12 | + end | ||
| 13 | +end |
spec/models/problem_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe Problem do | 3 | describe Problem do |
| 4 | + | ||
| 5 | + context 'validations' do | ||
| 6 | + it 'requires an environment' do | ||
| 7 | + err = Fabricate.build(:problem, :environment => nil) | ||
| 8 | + err.should_not be_valid | ||
| 9 | + err.errors[:environment].should include("can't be blank") | ||
| 10 | + end | ||
| 11 | + end | ||
| 12 | + | ||
| 4 | describe "Fabrication" do | 13 | describe "Fabrication" do |
| 5 | context "Fabricate(:problem)" do | 14 | context "Fabricate(:problem)" do |
| 6 | - it 'should be valid' do | ||
| 7 | - Fabricate.build(:problem).should be_valid | ||
| 8 | - end | ||
| 9 | it 'should have no comment' do | 15 | it 'should have no comment' do |
| 10 | lambda do | 16 | lambda do |
| 11 | Fabricate(:problem) | 17 | Fabricate(:problem) |
| @@ -14,15 +20,20 @@ describe Problem do | @@ -14,15 +20,20 @@ describe Problem do | ||
| 14 | end | 20 | end |
| 15 | 21 | ||
| 16 | context "Fabricate(:problem_with_comments)" do | 22 | context "Fabricate(:problem_with_comments)" do |
| 17 | - it 'should be valid' do | ||
| 18 | - Fabricate.build(:problem_with_comments).should be_valid | ||
| 19 | - end | ||
| 20 | it 'should have 3 comments' do | 23 | it 'should have 3 comments' do |
| 21 | lambda do | 24 | lambda do |
| 22 | Fabricate(:problem_with_comments) | 25 | Fabricate(:problem_with_comments) |
| 23 | end.should change(Comment, :count).by(3) | 26 | end.should change(Comment, :count).by(3) |
| 24 | end | 27 | end |
| 25 | end | 28 | end |
| 29 | + | ||
| 30 | + context "Fabricate(:problem_with_errs)" do | ||
| 31 | + it 'should have 3 errs' do | ||
| 32 | + lambda do | ||
| 33 | + Fabricate(:problem_with_errs) | ||
| 34 | + end.should change(Err, :count).by(3) | ||
| 35 | + end | ||
| 36 | + end | ||
| 26 | end | 37 | end |
| 27 | 38 | ||
| 28 | context '#last_notice_at' do | 39 | context '#last_notice_at' do |
| @@ -46,14 +57,13 @@ describe Problem do | @@ -46,14 +57,13 @@ describe Problem do | ||
| 46 | problem.should_not be_nil | 57 | problem.should_not be_nil |
| 47 | 58 | ||
| 48 | notice1 = Fabricate(:notice, :err => err) | 59 | notice1 = Fabricate(:notice, :err => err) |
| 49 | - problem.first_notice_at.should == notice1.created_at | 60 | + expect(problem.first_notice_at.to_i).to be_within(1).of(notice1.created_at.to_i) |
| 50 | 61 | ||
| 51 | notice2 = Fabricate(:notice, :err => err) | 62 | notice2 = Fabricate(:notice, :err => err) |
| 52 | - problem.first_notice_at.should == notice1.created_at | 63 | + expect(problem.first_notice_at.to_i).to be_within(1).of(notice1.created_at.to_i) |
| 53 | end | 64 | end |
| 54 | end | 65 | end |
| 55 | 66 | ||
| 56 | - | ||
| 57 | context '#message' do | 67 | context '#message' do |
| 58 | it "adding a notice caches its message" do | 68 | it "adding a notice caches its message" do |
| 59 | err = Fabricate(:err) | 69 | err = Fabricate(:err) |
| @@ -64,7 +74,6 @@ describe Problem do | @@ -64,7 +74,6 @@ describe Problem do | ||
| 64 | end | 74 | end |
| 65 | end | 75 | end |
| 66 | 76 | ||
| 67 | - | ||
| 68 | context 'being created' do | 77 | context 'being created' do |
| 69 | context 'when the app has err notifications set to false' do | 78 | context 'when the app has err notifications set to false' do |
| 70 | it 'should not send an email notification' do | 79 | it 'should not send an email notification' do |
| @@ -75,7 +84,6 @@ describe Problem do | @@ -75,7 +84,6 @@ describe Problem do | ||
| 75 | end | 84 | end |
| 76 | end | 85 | end |
| 77 | 86 | ||
| 78 | - | ||
| 79 | context "#resolved?" do | 87 | context "#resolved?" do |
| 80 | it "should start out as unresolved" do | 88 | it "should start out as unresolved" do |
| 81 | problem = Problem.new | 89 | problem = Problem.new |
| @@ -91,7 +99,6 @@ describe Problem do | @@ -91,7 +99,6 @@ describe Problem do | ||
| 91 | end | 99 | end |
| 92 | end | 100 | end |
| 93 | 101 | ||
| 94 | - | ||
| 95 | context "resolve!" do | 102 | context "resolve!" do |
| 96 | it "marks the problem as resolved" do | 103 | it "marks the problem as resolved" do |
| 97 | problem = Fabricate(:problem) | 104 | problem = Fabricate(:problem) |
| @@ -102,7 +109,7 @@ describe Problem do | @@ -102,7 +109,7 @@ describe Problem do | ||
| 102 | 109 | ||
| 103 | it "should record the time when it was resolved" do | 110 | it "should record the time when it was resolved" do |
| 104 | problem = Fabricate(:problem) | 111 | problem = Fabricate(:problem) |
| 105 | - expected_resolved_at = Time.now | 112 | + expected_resolved_at = Time.zone.now |
| 106 | Timecop.freeze(expected_resolved_at) do | 113 | Timecop.freeze(expected_resolved_at) do |
| 107 | problem.resolve! | 114 | problem.resolve! |
| 108 | end | 115 | end |
| @@ -121,12 +128,12 @@ describe Problem do | @@ -121,12 +128,12 @@ describe Problem do | ||
| 121 | it "should throw an err if it's not successful" do | 128 | it "should throw an err if it's not successful" do |
| 122 | problem = Fabricate(:problem) | 129 | problem = Fabricate(:problem) |
| 123 | problem.should_not be_resolved | 130 | problem.should_not be_resolved |
| 124 | - problem.stub!(:valid?).and_return(false) | 131 | + problem.stub(:valid?).and_return(false) |
| 125 | ## update_attributes not test #valid? but #errors.any? | 132 | ## update_attributes not test #valid? but #errors.any? |
| 126 | # https://github.com/mongoid/mongoid/blob/master/lib/mongoid/persistence.rb#L137 | 133 | # https://github.com/mongoid/mongoid/blob/master/lib/mongoid/persistence.rb#L137 |
| 127 | er = ActiveModel::Errors.new(problem) | 134 | er = ActiveModel::Errors.new(problem) |
| 128 | er.add_on_blank(:resolved) | 135 | er.add_on_blank(:resolved) |
| 129 | - problem.stub!(:errors).and_return(er) | 136 | + problem.stub(:errors).and_return(er) |
| 130 | problem.should_not be_valid | 137 | problem.should_not be_valid |
| 131 | lambda { | 138 | lambda { |
| 132 | problem.resolve! | 139 | problem.resolve! |
| @@ -134,22 +141,6 @@ describe Problem do | @@ -134,22 +141,6 @@ describe Problem do | ||
| 134 | end | 141 | end |
| 135 | end | 142 | end |
| 136 | 143 | ||
| 137 | - | ||
| 138 | - context ".merge!" do | ||
| 139 | - it "collects the Errs from several problems into one and deletes the other problems" do | ||
| 140 | - problem1 = Fabricate(:err).problem | ||
| 141 | - problem2 = Fabricate(:err).problem | ||
| 142 | - problem1.errs.length.should == 1 | ||
| 143 | - problem2.errs.length.should == 1 | ||
| 144 | - | ||
| 145 | - lambda { | ||
| 146 | - merged_problem = Problem.merge!(problem1, problem2) | ||
| 147 | - merged_problem.reload.errs.length.should == 2 | ||
| 148 | - }.should change(Problem, :count).by(-1) | ||
| 149 | - end | ||
| 150 | - end | ||
| 151 | - | ||
| 152 | - | ||
| 153 | context "#unmerge!" do | 144 | context "#unmerge!" do |
| 154 | it "creates a separate problem for each err" do | 145 | it "creates a separate problem for each err" do |
| 155 | problem1 = Fabricate(:notice).problem | 146 | problem1 = Fabricate(:notice).problem |
| @@ -166,7 +157,6 @@ describe Problem do | @@ -166,7 +157,6 @@ describe Problem do | ||
| 166 | end | 157 | end |
| 167 | end | 158 | end |
| 168 | 159 | ||
| 169 | - | ||
| 170 | context "Scopes" do | 160 | context "Scopes" do |
| 171 | context "resolved" do | 161 | context "resolved" do |
| 172 | it 'only finds resolved Problems' do | 162 | it 'only finds resolved Problems' do |
| @@ -185,8 +175,22 @@ describe Problem do | @@ -185,8 +175,22 @@ describe Problem do | ||
| 185 | Problem.unresolved.all.should include(unresolved) | 175 | Problem.unresolved.all.should include(unresolved) |
| 186 | end | 176 | end |
| 187 | end | 177 | end |
| 188 | - end | ||
| 189 | 178 | ||
| 179 | + context "searching" do | ||
| 180 | + it 'finds the correct record' do | ||
| 181 | + find = Fabricate(:problem, :resolved => false, :error_class => 'theErrorclass::other', | ||
| 182 | + :message => "other", :where => 'errorclass', :environment => 'development', :app_name => 'other') | ||
| 183 | + dont_find = Fabricate(:problem, :resolved => false, :error_class => "Batman", | ||
| 184 | + :message => 'todo', :where => 'classerror', :environment => 'development', :app_name => 'other') | ||
| 185 | + Problem.search("theErrorClass").unresolved.should include(find) | ||
| 186 | + Problem.search("theErrorClass").unresolved.should_not include(dont_find) | ||
| 187 | + end | ||
| 188 | + it 'find on where message' do | ||
| 189 | + problem = Fabricate(:problem, :where => 'cyril') | ||
| 190 | + Problem.search('cyril').entries.should eq [problem] | ||
| 191 | + end | ||
| 192 | + end | ||
| 193 | + end | ||
| 190 | 194 | ||
| 191 | context "notice counter cache" do | 195 | context "notice counter cache" do |
| 192 | before do | 196 | before do |
| @@ -202,19 +206,18 @@ describe Problem do | @@ -202,19 +206,18 @@ describe Problem do | ||
| 202 | it "adding a notice increases #notices_count by 1" do | 206 | it "adding a notice increases #notices_count by 1" do |
| 203 | lambda { | 207 | lambda { |
| 204 | Fabricate(:notice, :err => @err, :message => 'ERR 1') | 208 | Fabricate(:notice, :err => @err, :message => 'ERR 1') |
| 205 | - }.should change(@problem, :notices_count).from(0).to(1) | 209 | + }.should change(@problem.reload, :notices_count).from(0).to(1) |
| 206 | end | 210 | end |
| 207 | 211 | ||
| 208 | it "removing a notice decreases #notices_count by 1" do | 212 | it "removing a notice decreases #notices_count by 1" do |
| 209 | notice1 = Fabricate(:notice, :err => @err, :message => 'ERR 1') | 213 | notice1 = Fabricate(:notice, :err => @err, :message => 'ERR 1') |
| 210 | - lambda { | 214 | + expect { |
| 211 | @err.notices.first.destroy | 215 | @err.notices.first.destroy |
| 212 | @problem.reload | 216 | @problem.reload |
| 213 | - }.should change(@problem, :notices_count).from(1).to(0) | 217 | + }.to change(@problem, :notices_count).from(1).to(0) |
| 214 | end | 218 | end |
| 215 | end | 219 | end |
| 216 | 220 | ||
| 217 | - | ||
| 218 | context "#app_name" do | 221 | context "#app_name" do |
| 219 | let!(:app) { Fabricate(:app) } | 222 | let!(:app) { Fabricate(:app) } |
| 220 | let!(:problem) { Fabricate(:problem, :app => app) } | 223 | let!(:problem) { Fabricate(:problem, :app => app) } |
| @@ -328,7 +331,7 @@ describe Problem do | @@ -328,7 +331,7 @@ describe Problem do | ||
| 328 | it "adding a notice adds a string to #user_agents" do | 331 | it "adding a notice adds a string to #user_agents" do |
| 329 | lambda { | 332 | lambda { |
| 330 | Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) | 333 | Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) |
| 331 | - }.should change(@problem, :user_agents).from({}).to({Digest::MD5.hexdigest('Chrome 10.0.648.204') => {'value' => 'Chrome 10.0.648.204', 'count' => 1}}) | 334 | + }.should change(@problem, :user_agents).from({}).to({Digest::MD5.hexdigest('Chrome 10.0.648.204 (OS X 10.6.7)') => {'value' => 'Chrome 10.0.648.204 (OS X 10.6.7)', 'count' => 1}}) |
| 332 | end | 335 | end |
| 333 | 336 | ||
| 334 | it "removing a notice removes string from #user_agents" do | 337 | it "removing a notice removes string from #user_agents" do |
| @@ -336,7 +339,9 @@ describe Problem do | @@ -336,7 +339,9 @@ describe Problem do | ||
| 336 | lambda { | 339 | lambda { |
| 337 | @err.notices.first.destroy | 340 | @err.notices.first.destroy |
| 338 | @problem.reload | 341 | @problem.reload |
| 339 | - }.should change(@problem, :user_agents).from({Digest::MD5.hexdigest('Chrome 10.0.648.204') => {'value' => 'Chrome 10.0.648.204', 'count' => 1}}).to({}) | 342 | + }.should change(@problem, :user_agents).from({ |
| 343 | + Digest::MD5.hexdigest('Chrome 10.0.648.204 (OS X 10.6.7)') => {'value' => 'Chrome 10.0.648.204 (OS X 10.6.7)', 'count' => 1} | ||
| 344 | + }).to({}) | ||
| 340 | end | 345 | end |
| 341 | end | 346 | end |
| 342 | 347 |
spec/models/user_spec.rb
| @@ -29,6 +29,15 @@ describe User do | @@ -29,6 +29,15 @@ describe User do | ||
| 29 | user2.should_not be_valid | 29 | user2.should_not be_valid |
| 30 | user2.errors[:github_login].should include("is already taken") | 30 | user2.errors[:github_login].should include("is already taken") |
| 31 | end | 31 | end |
| 32 | + | ||
| 33 | + it 'allows blank / null github_login' do | ||
| 34 | + user1 = Fabricate(:user, :github_login => ' ') | ||
| 35 | + user1.should be_valid | ||
| 36 | + | ||
| 37 | + user2 = Fabricate.build(:user, :github_login => ' ') | ||
| 38 | + user2.save | ||
| 39 | + user2.should be_valid | ||
| 40 | + end | ||
| 32 | end | 41 | end |
| 33 | 42 | ||
| 34 | context 'Watchers' do | 43 | context 'Watchers' do |
| @@ -40,15 +49,6 @@ describe User do | @@ -40,15 +49,6 @@ describe User do | ||
| 40 | user.watchers.should include(watcher) | 49 | user.watchers.should include(watcher) |
| 41 | end | 50 | end |
| 42 | 51 | ||
| 43 | - it "destroys any related watchers when it is destroyed" do | ||
| 44 | - user = Fabricate(:user) | ||
| 45 | - app = Fabricate(:app) | ||
| 46 | - watcher = Fabricate(:user_watcher, :app => app, :user => user) | ||
| 47 | - user.watchers.should_not be_empty | ||
| 48 | - user.destroy | ||
| 49 | - app.reload.watchers.should_not include(watcher) | ||
| 50 | - end | ||
| 51 | - | ||
| 52 | it "has many apps through watchers" do | 52 | it "has many apps through watchers" do |
| 53 | user = Fabricate(:user) | 53 | user = Fabricate(:user) |
| 54 | watched_app = Fabricate(:app) | 54 | watched_app = Fabricate(:app) |
| @@ -62,10 +62,12 @@ describe User do | @@ -62,10 +62,12 @@ describe User do | ||
| 62 | 62 | ||
| 63 | context "First user" do | 63 | context "First user" do |
| 64 | it "should be created this admin access via db:seed" do | 64 | it "should be created this admin access via db:seed" do |
| 65 | - require 'rake' | ||
| 66 | - Errbit::Application.load_tasks | ||
| 67 | - Rake::Task["db:seed"].execute | ||
| 68 | - User.first.admin.should be_true | 65 | + expect { |
| 66 | + $stdout.stub(:puts => true) | ||
| 67 | + require Rails.root.join('db/seeds.rb') | ||
| 68 | + }.to change { | ||
| 69 | + User.where(:admin => true).count | ||
| 70 | + }.from(0).to(1) | ||
| 69 | end | 71 | end |
| 70 | end | 72 | end |
| 71 | 73 |
| @@ -0,0 +1,50 @@ | @@ -0,0 +1,50 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "Notices management" do | ||
| 4 | + | ||
| 5 | + let(:errbit_app) { Fabricate(:app, | ||
| 6 | + :api_key => 'APIKEY') } | ||
| 7 | + | ||
| 8 | + describe "create a new notice" do | ||
| 9 | + context "with valide notice" do | ||
| 10 | + let(:xml) { Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read } | ||
| 11 | + it 'save a new notice' do | ||
| 12 | + expect { | ||
| 13 | + post '/notifier_api/v2/notices', :data => xml | ||
| 14 | + expect(response).to be_success | ||
| 15 | + }.to change { | ||
| 16 | + errbit_app.problems.count | ||
| 17 | + }.by(1) | ||
| 18 | + end | ||
| 19 | + end | ||
| 20 | + | ||
| 21 | + context "with notice with empty backtrace" do | ||
| 22 | + let(:xml) { Rails.root.join('spec','fixtures','hoptoad_test_notice_without_line_of_backtrace.xml').read } | ||
| 23 | + it 'save a new notice' do | ||
| 24 | + expect { | ||
| 25 | + post '/notifier_api/v2/notices', :data => xml | ||
| 26 | + expect(response).to be_success | ||
| 27 | + }.to change { | ||
| 28 | + errbit_app.problems.count | ||
| 29 | + }.by(1) | ||
| 30 | + end | ||
| 31 | + end | ||
| 32 | + | ||
| 33 | + context "with notice with bad api_key" do | ||
| 34 | + let(:errbit_app) { Fabricate(:app) } | ||
| 35 | + let(:xml) { Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read } | ||
| 36 | + it 'not save a new notice and return 422' do | ||
| 37 | + expect { | ||
| 38 | + post '/notifier_api/v2/notices', :data => xml | ||
| 39 | + expect(response.status).to eq 422 | ||
| 40 | + expect(response.body).to eq "Your API key is unknown" | ||
| 41 | + }.to_not change { | ||
| 42 | + errbit_app.problems.count | ||
| 43 | + }.by(1) | ||
| 44 | + end | ||
| 45 | + | ||
| 46 | + end | ||
| 47 | + | ||
| 48 | + end | ||
| 49 | + | ||
| 50 | +end |
spec/spec_helper.rb
| 1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec' | 1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec' |
| 2 | # from the project root directory. | 2 | # from the project root directory. |
| 3 | ENV["RAILS_ENV"] ||= 'test' | 3 | ENV["RAILS_ENV"] ||= 'test' |
| 4 | + | ||
| 5 | +if ENV['COVERAGE'] | ||
| 6 | + require 'coveralls' | ||
| 7 | + require 'simplecov' | ||
| 8 | + Coveralls.wear!('rails') do | ||
| 9 | + add_filter 'bundle' | ||
| 10 | + end | ||
| 11 | + SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ | ||
| 12 | + SimpleCov::Formatter::HTMLFormatter, | ||
| 13 | + Coveralls::SimpleCov::Formatter | ||
| 14 | + ] | ||
| 15 | + SimpleCov.start('rails') do | ||
| 16 | + add_filter 'bundle' | ||
| 17 | + end | ||
| 18 | +end | ||
| 19 | + | ||
| 4 | require File.expand_path("../../config/environment", __FILE__) | 20 | require File.expand_path("../../config/environment", __FILE__) |
| 5 | require 'rspec/rails' | 21 | require 'rspec/rails' |
| 6 | require 'database_cleaner' | 22 | require 'database_cleaner' |
| 7 | require 'webmock/rspec' | 23 | require 'webmock/rspec' |
| 8 | require 'xmpp4r' | 24 | require 'xmpp4r' |
| 25 | +require 'xmpp4r/muc' | ||
| 26 | +require 'mongoid-rspec' | ||
| 27 | + | ||
| 9 | 28 | ||
| 10 | # Requires supporting files with custom matchers and macros, etc, | 29 | # Requires supporting files with custom matchers and macros, etc, |
| 11 | # in ./support/ and its subdirectories. | 30 | # in ./support/ and its subdirectories. |
| @@ -18,6 +37,7 @@ end | @@ -18,6 +37,7 @@ end | ||
| 18 | RSpec.configure do |config| | 37 | RSpec.configure do |config| |
| 19 | config.mock_with :rspec | 38 | config.mock_with :rspec |
| 20 | config.include Devise::TestHelpers, :type => :controller | 39 | config.include Devise::TestHelpers, :type => :controller |
| 40 | + config.include Mongoid::Matchers, :type => :model | ||
| 21 | config.filter_run :focused => true | 41 | config.filter_run :focused => true |
| 22 | config.run_all_when_everything_filtered = true | 42 | config.run_all_when_everything_filtered = true |
| 23 | config.alias_example_to :fit, :focused => true | 43 | config.alias_example_to :fit, :focused => true |
| @@ -27,6 +47,16 @@ RSpec.configure do |config| | @@ -27,6 +47,16 @@ RSpec.configure do |config| | ||
| 27 | DatabaseCleaner.clean | 47 | DatabaseCleaner.clean |
| 28 | end | 48 | end |
| 29 | config.include WebMock::API | 49 | config.include WebMock::API |
| 50 | + | ||
| 51 | + config.include Haml, :type => :helper | ||
| 52 | + config.include Haml::Helpers, :type => :helper | ||
| 53 | + config.before(:each, :type => :helper) do |config| | ||
| 54 | + init_haml_helpers | ||
| 55 | + end | ||
| 56 | + | ||
| 57 | + config.after(:all) do | ||
| 58 | + WebMock.disable_net_connect! :allow => /coveralls\.io/ | ||
| 59 | + end | ||
| 30 | end | 60 | end |
| 31 | 61 | ||
| 32 | OmniAuth.config.test_mode = true | 62 | OmniAuth.config.test_mode = true |
| @@ -0,0 +1,37 @@ | @@ -0,0 +1,37 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "apps/edit.html.haml" do | ||
| 4 | + let(:app) { stub_model(App) } | ||
| 5 | + before do | ||
| 6 | + view.stub(:app).and_return(app) | ||
| 7 | + controller.stub(:current_user) { stub_model(User) } | ||
| 8 | + end | ||
| 9 | + | ||
| 10 | + describe "content_for :action_bar" do | ||
| 11 | + def action_bar | ||
| 12 | + view.content_for(:action_bar) | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + it "should confirm the 'destroy' link" do | ||
| 16 | + render | ||
| 17 | + | ||
| 18 | + action_bar.should have_selector('a.button[data-confirm="Seriously?"]') | ||
| 19 | + end | ||
| 20 | + | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + context "with unvalid app" do | ||
| 24 | + let(:app) { | ||
| 25 | + app = stub_model(App) | ||
| 26 | + app.errors.add(:base,'You must specify your') | ||
| 27 | + app | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + it 'see the error' do | ||
| 31 | + render | ||
| 32 | + rendered.should match(/You must specify your/) | ||
| 33 | + end | ||
| 34 | + end | ||
| 35 | + | ||
| 36 | +end | ||
| 37 | + |
spec/views/apps/index.html.haml_spec.rb
| @@ -3,7 +3,7 @@ require 'spec_helper' | @@ -3,7 +3,7 @@ require 'spec_helper' | ||
| 3 | describe "apps/index.html.haml" do | 3 | describe "apps/index.html.haml" do |
| 4 | before do | 4 | before do |
| 5 | app = stub_model(App, :deploys => [stub_model(Deploy, :created_at => Time.now, :revision => "123456789abcdef")]) | 5 | app = stub_model(App, :deploys => [stub_model(Deploy, :created_at => Time.now, :revision => "123456789abcdef")]) |
| 6 | - assign :apps, [app] | 6 | + view.stub(:apps).and_return([app]) |
| 7 | controller.stub(:current_user) { stub_model(User) } | 7 | controller.stub(:current_user) { stub_model(User) } |
| 8 | end | 8 | end |
| 9 | 9 |
| @@ -0,0 +1,37 @@ | @@ -0,0 +1,37 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "apps/new.html.haml" do | ||
| 4 | + let(:app) { stub_model(App) } | ||
| 5 | + before do | ||
| 6 | + view.stub(:app).and_return(app) | ||
| 7 | + controller.stub(:current_user) { stub_model(User) } | ||
| 8 | + end | ||
| 9 | + | ||
| 10 | + describe "content_for :action_bar" do | ||
| 11 | + def action_bar | ||
| 12 | + view.content_for(:action_bar) | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + it "should confirm the 'cancel' link" do | ||
| 16 | + render | ||
| 17 | + | ||
| 18 | + action_bar.should have_selector('a.button', :text => 'cancel') | ||
| 19 | + end | ||
| 20 | + | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + context "with unvalid app" do | ||
| 24 | + let(:app) { | ||
| 25 | + app = stub_model(App) | ||
| 26 | + app.errors.add(:base,'You must specify your') | ||
| 27 | + app | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + it 'see the error' do | ||
| 31 | + render | ||
| 32 | + rendered.should match(/You must specify your/) | ||
| 33 | + end | ||
| 34 | + end | ||
| 35 | + | ||
| 36 | +end | ||
| 37 | + |
| @@ -0,0 +1,21 @@ | @@ -0,0 +1,21 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "apps/show.atom.builder" do | ||
| 4 | + let(:app) { stub_model(App) } | ||
| 5 | + let(:problems) { [ | ||
| 6 | + stub_model(Problem, :message => 'foo', :app => app) | ||
| 7 | + ]} | ||
| 8 | + | ||
| 9 | + before do | ||
| 10 | + view.stub(:app).and_return(app) | ||
| 11 | + view.stub(:problems).and_return(problems) | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + context "with errs" do | ||
| 15 | + it 'see the errs message' do | ||
| 16 | + render | ||
| 17 | + expect(rendered).to match(problems.first.message) | ||
| 18 | + end | ||
| 19 | + end | ||
| 20 | + | ||
| 21 | +end |
| @@ -0,0 +1,32 @@ | @@ -0,0 +1,32 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "apps/show.html.haml" do | ||
| 4 | + let(:app) { stub_model(App) } | ||
| 5 | + before do | ||
| 6 | + view.stub(:app).and_return(app) | ||
| 7 | + view.stub(:all_errs).and_return(false) | ||
| 8 | + view.stub(:deploys).and_return([]) | ||
| 9 | + controller.stub(:current_user) { stub_model(User) } | ||
| 10 | + end | ||
| 11 | + | ||
| 12 | + describe "content_for :action_bar" do | ||
| 13 | + def action_bar | ||
| 14 | + view.content_for(:action_bar) | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + it "should confirm the 'cancel' link" do | ||
| 18 | + render | ||
| 19 | + | ||
| 20 | + action_bar.should have_selector('a.button', :text => 'all errs') | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + end | ||
| 24 | + | ||
| 25 | + context "without errs" do | ||
| 26 | + it 'see no errs' do | ||
| 27 | + render | ||
| 28 | + rendered.should match(/No errs have been/) | ||
| 29 | + end | ||
| 30 | + end | ||
| 31 | +end | ||
| 32 | + |
| @@ -0,0 +1,12 @@ | @@ -0,0 +1,12 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "notices/_summary.html.haml" do | ||
| 4 | + let(:notice) { Fabricate(:notice, :framework => 'Rails 3.2.11') } | ||
| 5 | + | ||
| 6 | + it "renders application framework" do | ||
| 7 | + render "notices/summary", :notice => notice, :problem => notice.problem | ||
| 8 | + | ||
| 9 | + rendered.should have_content('Rails 3.2.11') | ||
| 10 | + end | ||
| 11 | +end | ||
| 12 | + |
| @@ -0,0 +1,14 @@ | @@ -0,0 +1,14 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "problems/index.atom.builder" do | ||
| 4 | + | ||
| 5 | + it 'display problem message' do | ||
| 6 | + app = App.new(:new_record => false) | ||
| 7 | + view.stub(:problems).and_return([Problem.new( | ||
| 8 | + :message => 'foo', | ||
| 9 | + :new_record => false, :app => app), Problem.new(:new_record => false, :app => app)]) | ||
| 10 | + render | ||
| 11 | + rendered.should match('foo') | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | +end |
| @@ -0,0 +1,25 @@ | @@ -0,0 +1,25 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "problems/index.html.haml" do | ||
| 4 | + let(:problem_1) { Fabricate(:problem) } | ||
| 5 | + let(:problem_2) { Fabricate(:problem, :app => problem_1.app) } | ||
| 6 | + | ||
| 7 | + before do | ||
| 8 | + # view.stub(:app).and_return(problem.app) | ||
| 9 | + view.stub(:selected_problems).and_return([]) | ||
| 10 | + view.stub(:problems).and_return(Kaminari.paginate_array([problem_1, problem_2]).page(1).per(10)) | ||
| 11 | + view.stub(:params_sort).and_return('asc') | ||
| 12 | + controller.stub(:current_user) { Fabricate(:user) } | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + describe "with problem" do | ||
| 16 | + before { problem_1 && problem_2 } | ||
| 17 | + | ||
| 18 | + it 'should works' do | ||
| 19 | + render | ||
| 20 | + rendered.should have_selector('div#problem_table.problem_table') | ||
| 21 | + end | ||
| 22 | + end | ||
| 23 | + | ||
| 24 | +end | ||
| 25 | + |
spec/views/problems/show.html.haml_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe "problems/show.html.haml" do | 3 | describe "problems/show.html.haml" do |
| 4 | + let(:problem) { Fabricate(:problem) } | ||
| 5 | + let(:comment) { Fabricate(:comment) } | ||
| 6 | + | ||
| 4 | before do | 7 | before do |
| 5 | - problem = Fabricate(:problem) | ||
| 6 | - comment = Fabricate(:comment) | ||
| 7 | - assign :problem, problem | 8 | + view.stub(:app).and_return(problem.app) |
| 9 | + view.stub(:problem).and_return(problem) | ||
| 10 | + | ||
| 8 | assign :comment, comment | 11 | assign :comment, comment |
| 9 | - assign :app, problem.app | ||
| 10 | assign :notices, problem.notices.page(1).per(1) | 12 | assign :notices, problem.notices.page(1).per(1) |
| 11 | assign :notice, problem.notices.first | 13 | assign :notice, problem.notices.first |
| 14 | + | ||
| 12 | controller.stub(:current_user) { Fabricate(:user) } | 15 | controller.stub(:current_user) { Fabricate(:user) } |
| 13 | end | 16 | end |
| 14 | 17 | ||
| 15 | def with_issue_tracker(tracker, problem) | 18 | def with_issue_tracker(tracker, problem) |
| 16 | problem.app.issue_tracker = tracker.new :api_token => "token token token", :project_id => "1234" | 19 | problem.app.issue_tracker = tracker.new :api_token => "token token token", :project_id => "1234" |
| 17 | - assign :problem, problem | ||
| 18 | - assign :app, problem.app | 20 | + view.stub(:problem).and_return(problem) |
| 21 | + view.stub(:app).and_return(problem.app) | ||
| 19 | end | 22 | end |
| 20 | 23 | ||
| 21 | describe "content_for :action_bar" do | 24 | describe "content_for :action_bar" do |
| @@ -54,8 +57,8 @@ describe "problems/show.html.haml" do | @@ -54,8 +57,8 @@ describe "problems/show.html.haml" do | ||
| 54 | it "should link 'up' to app_problems_path if HTTP_REFERER isn't set'" do | 57 | it "should link 'up' to app_problems_path if HTTP_REFERER isn't set'" do |
| 55 | controller.request.env['HTTP_REFERER'] = nil | 58 | controller.request.env['HTTP_REFERER'] = nil |
| 56 | problem = Fabricate(:problem_with_comments) | 59 | problem = Fabricate(:problem_with_comments) |
| 57 | - assign :problem, problem | ||
| 58 | - assign :app, problem.app | 60 | + view.stub(:problem).and_return(problem) |
| 61 | + view.stub(:app).and_return(problem.app) | ||
| 59 | render | 62 | render |
| 60 | 63 | ||
| 61 | action_bar.should have_selector("span a.up[href='#{app_problems_path(problem.app)}']", :text => 'up') | 64 | action_bar.should have_selector("span a.up[href='#{app_problems_path(problem.app)}']", :text => 'up') |
| @@ -67,8 +70,8 @@ describe "problems/show.html.haml" do | @@ -67,8 +70,8 @@ describe "problems/show.html.haml" do | ||
| 67 | controller.stub(:current_user) { user } | 70 | controller.stub(:current_user) { user } |
| 68 | 71 | ||
| 69 | problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo")) | 72 | problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo")) |
| 70 | - assign :problem, problem | ||
| 71 | - assign :app, problem.app | 73 | + view.stub(:problem).and_return(problem) |
| 74 | + view.stub(:app).and_return(problem.app) | ||
| 72 | render | 75 | render |
| 73 | 76 | ||
| 74 | action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue') | 77 | action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue') |
| @@ -77,12 +80,54 @@ describe "problems/show.html.haml" do | @@ -77,12 +80,54 @@ describe "problems/show.html.haml" do | ||
| 77 | it 'should allow creating issue for github if application has a github tracker' do | 80 | it 'should allow creating issue for github if application has a github tracker' do |
| 78 | problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo")) | 81 | problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo")) |
| 79 | with_issue_tracker(GithubIssuesTracker, problem) | 82 | with_issue_tracker(GithubIssuesTracker, problem) |
| 80 | - assign :problem, problem | ||
| 81 | - assign :app, problem.app | 83 | + view.stub(:problem).and_return(problem) |
| 84 | + view.stub(:app).and_return(problem.app) | ||
| 82 | render | 85 | render |
| 83 | 86 | ||
| 84 | action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue') | 87 | action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue') |
| 85 | end | 88 | end |
| 89 | + | ||
| 90 | + context "without issue tracker associate on app" do | ||
| 91 | + let(:problem){ Problem.new(:new_record => false, :app => app) } | ||
| 92 | + let(:app) { App.new(:new_record => false) } | ||
| 93 | + | ||
| 94 | + it 'not see link to create issue' do | ||
| 95 | + view.stub(:problem).and_return(problem) | ||
| 96 | + view.stub(:app).and_return(problem.app) | ||
| 97 | + render | ||
| 98 | + expect(view.content_for(:action_bar)).to_not match(/create issue/) | ||
| 99 | + end | ||
| 100 | + | ||
| 101 | + end | ||
| 102 | + | ||
| 103 | + context "with lighthouse tracker on app" do | ||
| 104 | + let(:app) { App.new(:new_record => false, :issue_tracker => tracker ) } | ||
| 105 | + let(:tracker) { | ||
| 106 | + IssueTrackers::LighthouseTracker.new(:project_id => 'x') | ||
| 107 | + } | ||
| 108 | + context "with problem without issue link" do | ||
| 109 | + let(:problem){ Problem.new(:new_record => false, :app => app) } | ||
| 110 | + it 'not see link if no issue tracker' do | ||
| 111 | + view.stub(:problem).and_return(problem) | ||
| 112 | + view.stub(:app).and_return(problem.app) | ||
| 113 | + render | ||
| 114 | + expect(view.content_for(:action_bar)).to match(/create issue/) | ||
| 115 | + end | ||
| 116 | + | ||
| 117 | + end | ||
| 118 | + | ||
| 119 | + context "with problem with issue link" do | ||
| 120 | + let(:problem){ Problem.new(:new_record => false, :app => app, :issue_link => 'http://foo') } | ||
| 121 | + | ||
| 122 | + it 'not see link if no issue tracker' do | ||
| 123 | + view.stub(:problem).and_return(problem) | ||
| 124 | + view.stub(:app).and_return(problem.app) | ||
| 125 | + render | ||
| 126 | + expect(view.content_for(:action_bar)).to_not match(/create issue/) | ||
| 127 | + end | ||
| 128 | + end | ||
| 129 | + | ||
| 130 | + end | ||
| 86 | end | 131 | end |
| 87 | end | 132 | end |
| 88 | 133 | ||
| @@ -94,8 +139,8 @@ describe "problems/show.html.haml" do | @@ -94,8 +139,8 @@ describe "problems/show.html.haml" do | ||
| 94 | 139 | ||
| 95 | it 'should display comments and new comment form when no issue tracker' do | 140 | it 'should display comments and new comment form when no issue tracker' do |
| 96 | problem = Fabricate(:problem_with_comments) | 141 | problem = Fabricate(:problem_with_comments) |
| 97 | - assign :problem, problem | ||
| 98 | - assign :app, problem.app | 142 | + view.stub(:problem).and_return(problem) |
| 143 | + view.stub(:app).and_return(problem.app) | ||
| 99 | render | 144 | render |
| 100 | 145 | ||
| 101 | view.content_for(:comments).should include('Test comment') | 146 | view.content_for(:comments).should include('Test comment') |
| @@ -0,0 +1,14 @@ | @@ -0,0 +1,14 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe "problems/show.html.ics" do | ||
| 4 | + let(:problem) { Fabricate(:problem) } | ||
| 5 | + before do | ||
| 6 | + view.stub(:problem).and_return(problem) | ||
| 7 | + end | ||
| 8 | + | ||
| 9 | + it 'should work' do | ||
| 10 | + render :template => 'problems/show', :formats => [:ics], :handlers => [:haml] | ||
| 11 | + end | ||
| 12 | + | ||
| 13 | + | ||
| 14 | +end |
| @@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe 'users/edit.html.haml' do | ||
| 4 | + let(:user) { stub_model(User, :name => 'shingara') } | ||
| 5 | + before { | ||
| 6 | + view.stub(:current_user).and_return(user) | ||
| 7 | + view.stub(:user).and_return(user) | ||
| 8 | + } | ||
| 9 | + it 'should have per_page option' do | ||
| 10 | + render | ||
| 11 | + expect(rendered).to match(/id="user_per_page"/) | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + it 'should have time_zone option' do | ||
| 15 | + render | ||
| 16 | + expect(rendered).to match(/id="user_time_zone"/) | ||
| 17 | + end | ||
| 18 | +end |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe 'users/index.html.haml' do | ||
| 4 | + let(:user) { stub_model(User) } | ||
| 5 | + before { | ||
| 6 | + view.stub(:current_user).and_return(user) | ||
| 7 | + view.stub(:users).and_return( | ||
| 8 | + Kaminari.paginate_array([user], :total_count => 1).page(1) | ||
| 9 | + ) | ||
| 10 | + } | ||
| 11 | + it 'should see users option' do | ||
| 12 | + render | ||
| 13 | + expect(rendered).to match(/class='user_list'/) | ||
| 14 | + end | ||
| 15 | + | ||
| 16 | +end |
| @@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
| 1 | +require 'spec_helper' | ||
| 2 | + | ||
| 3 | +describe 'users/new.html.haml' do | ||
| 4 | + let(:user) { stub_model(User) } | ||
| 5 | + before { | ||
| 6 | + view.stub(:current_user).and_return(user) | ||
| 7 | + view.stub(:user).and_return(user) | ||
| 8 | + } | ||
| 9 | + it 'should have per_page option' do | ||
| 10 | + render | ||
| 11 | + expect(rendered).to match(/id="user_per_page"/) | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + it 'should have time_zone option' do | ||
| 15 | + render | ||
| 16 | + expect(rendered).to match(/id="user_time_zone"/) | ||
| 17 | + end | ||
| 18 | +end |
spec/views/users/show.html.haml_spec.rb
| 1 | require 'spec_helper' | 1 | require 'spec_helper' |
| 2 | 2 | ||
| 3 | describe 'users/show.html.haml' do | 3 | describe 'users/show.html.haml' do |
| 4 | + | ||
| 4 | let(:user) do | 5 | let(:user) do |
| 5 | stub_model(User, :created_at => Time.now, :email => "test@example.com") | 6 | stub_model(User, :created_at => Time.now, :email => "test@example.com") |
| 6 | end | 7 | end |
| @@ -8,12 +9,12 @@ describe 'users/show.html.haml' do | @@ -8,12 +9,12 @@ describe 'users/show.html.haml' do | ||
| 8 | before do | 9 | before do |
| 9 | Errbit::Config.stub(:github_authentication) { true } | 10 | Errbit::Config.stub(:github_authentication) { true } |
| 10 | controller.stub(:current_user) { stub_model(User) } | 11 | controller.stub(:current_user) { stub_model(User) } |
| 12 | + view.stub(:user) { user } | ||
| 11 | end | 13 | end |
| 12 | 14 | ||
| 13 | context 'with GitHub authentication' do | 15 | context 'with GitHub authentication' do |
| 14 | it 'shows github login' do | 16 | it 'shows github login' do |
| 15 | user.github_login = 'test_user' | 17 | user.github_login = 'test_user' |
| 16 | - assign :user, user | ||
| 17 | render | 18 | render |
| 18 | rendered.should match(/GitHub/) | 19 | rendered.should match(/GitHub/) |
| 19 | rendered.should match(/test_user/) | 20 | rendered.should match(/test_user/) |
| @@ -21,7 +22,6 @@ describe 'users/show.html.haml' do | @@ -21,7 +22,6 @@ describe 'users/show.html.haml' do | ||
| 21 | 22 | ||
| 22 | it 'does not show github if blank' do | 23 | it 'does not show github if blank' do |
| 23 | user.github_login = ' ' | 24 | user.github_login = ' ' |
| 24 | - assign :user, user | ||
| 25 | render | 25 | render |
| 26 | rendered.should_not match(/GitHub/) | 26 | rendered.should_not match(/GitHub/) |
| 27 | end | 27 | end |
| @@ -30,7 +30,6 @@ describe 'users/show.html.haml' do | @@ -30,7 +30,6 @@ describe 'users/show.html.haml' do | ||
| 30 | context "Linking GitHub account" do | 30 | context "Linking GitHub account" do |
| 31 | context 'viewing another user page' do | 31 | context 'viewing another user page' do |
| 32 | it "doesn't show and github linking buttons if user is not current user" do | 32 | it "doesn't show and github linking buttons if user is not current user" do |
| 33 | - assign :user, user | ||
| 34 | render | 33 | render |
| 35 | view.content_for(:action_bar).should_not include('Link GitHub account') | 34 | view.content_for(:action_bar).should_not include('Link GitHub account') |
| 36 | view.content_for(:action_bar).should_not include('Unlink GitHub account') | 35 | view.content_for(:action_bar).should_not include('Unlink GitHub account') |
| @@ -40,7 +39,6 @@ describe 'users/show.html.haml' do | @@ -40,7 +39,6 @@ describe 'users/show.html.haml' do | ||
| 40 | context 'viewing own user page' do | 39 | context 'viewing own user page' do |
| 41 | before do | 40 | before do |
| 42 | controller.stub(:current_user) { user } | 41 | controller.stub(:current_user) { user } |
| 43 | - assign :user, user | ||
| 44 | end | 42 | end |
| 45 | 43 | ||
| 46 | it 'shows link github button when no login or token' do | 44 | it 'shows link github button when no login or token' do |