Commit 09f663fc665a493e68fa17a7c9dbd0361733aa4d

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

Merge branch 'mongoid-3' into pr/289

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

1.18 KB

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

1.18 KB

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

1.1 KB

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

4.96 KB

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

4.96 KB

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

4.78 KB

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

1.97 KB

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

1.97 KB

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

1.97 KB

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

1.91 KB

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

2.29 KB

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

2.29 KB

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

2.11 KB

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

3.16 KB

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

3.16 KB

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

2.49 KB

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

45.1 KB

docs/notifications/flowdock/index.md 0 → 100644
@@ -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 +![flow token](flow_token_api.jpg)
  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/errbit/version.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +module Errbit::Version
  2 + MAJOR = 0
  3 + MINOR = 1
  4 + PATCH = 0
  5 +
  6 + def self.to_s
  7 + "#{MAJOR}.#{MINOR}.#{PATCH}"
  8 + end
  9 +end
lib/hoptoad.rb
@@ -3,7 +3,7 @@ require &#39;hoptoad/v2&#39; @@ -3,7 +3,7 @@ require &#39;hoptoad/v2&#39;
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, '&#38;')  
196 - .replace(/</g, '&#60;')  
197 - .replace(/>/g, '&#62;')  
198 - .replace(/'/g, '&#39;')  
199 - .replace(/"/g, '&#34;');  
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, '&#38;').replace(/</g, '&#60;').replace(/>/g, '&#62;')
  615 + .replace(/'/g, '&#39;').replace(/"/g, '&#34;');
  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);
script/rspec-queue-mongoid.rb 0 → 100755
@@ -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 &#39;Sign in with GitHub&#39; do @@ -11,7 +11,7 @@ feature &#39;Sign in with GitHub&#39; 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 &#39;spec_helper&#39; @@ -3,57 +3,63 @@ require &#39;spec_helper&#39;
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 &#39;spec_helper&#39; @@ -3,7 +3,7 @@ require &#39;spec_helper&#39;
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("&#8203;", "").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
1 Fabricator :err do 1 Fabricator :err do
2 problem! 2 problem!
3 - error_class! { 'FooError' }  
4 - component 'foo'  
5 - action 'bar'  
6 - environment 'production' 3 + fingerprint 'some-finger-print'
7 end 4 end
8 5
9 Fabricator :notice do 6 Fabricator :notice do
spec/fabricators/issue_tracker_fabricator.rb
@@ -33,3 +33,8 @@ Fabricator :bitbucket_issues_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;I @@ -33,3 +33,8 @@ Fabricator :bitbucket_issues_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;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 =&gt; :problem) do @@ -10,3 +12,13 @@ Fabricator(:problem_with_comments, :from =&gt; :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">#&lt;StringIO:0x103d9dec0&gt;</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">#&lt;ApplicationController:0x103d2f560&gt;</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">#&lt;StringIO:0x103d9dc90&gt;</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 &#39;spec_helper&#39; @@ -10,3 +10,12 @@ require &#39;spec_helper&#39;
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
spec/helpers/backtrace_line_helper.rb 0 → 100644
@@ -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
spec/interactors/problem_merge_spec.rb 0 → 100644
@@ -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
spec/interactors/problem_updater_cache_spec.rb 0 → 100644
@@ -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
spec/interactors/resolved_problem_clearer_spec.rb 0 → 100644
@@ -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
spec/interactors/user_destroy_spec.rb 0 → 100644
@@ -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 &lt; ActionController::Base") 27 + @email.should have_body_text("class &lt; 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 &#39;spec_helper&#39; @@ -3,12 +3,25 @@ require &#39;spec_helper&#39;
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
spec/models/backtrace_line_spec.rb 0 → 100644
@@ -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 }
spec/models/comment_observer_spec.rb 0 → 100644
@@ -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
spec/models/error_report_spec.rb 0 → 100644
@@ -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
spec/models/fabricators_spec.rb 0 → 100644
@@ -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
spec/models/fingerprint_spec.rb 0 → 100644
@@ -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('&lt;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('&lt;3') 21 message.should include('&lt;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
spec/models/notification_service/hubot_service_spec.rb 0 → 100644
@@ -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
spec/requests/notices_controller_spec.rb 0 → 100644
@@ -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
spec/views/apps/edit.html.haml_spec.rb 0 → 100644
@@ -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 &#39;spec_helper&#39; @@ -3,7 +3,7 @@ require &#39;spec_helper&#39;
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
spec/views/apps/new.html.haml_spec.rb 0 → 100644
@@ -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 +
spec/views/apps/show.atom.builder_spec.rb 0 → 100644
@@ -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
spec/views/apps/show.html.haml_spec.rb 0 → 100644
@@ -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 +
spec/views/notices/_summary.html.haml_spec.rb 0 → 100644
@@ -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 +
spec/views/problems/index.atom.builder_spec.rb 0 → 100644
@@ -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
spec/views/problems/index.html.haml_spec.rb 0 → 100644
@@ -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 &quot;problems/show.html.haml&quot; do @@ -54,8 +57,8 @@ describe &quot;problems/show.html.haml&quot; 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 &quot;problems/show.html.haml&quot; do @@ -67,8 +70,8 @@ describe &quot;problems/show.html.haml&quot; 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 &quot;problems/show.html.haml&quot; do @@ -77,12 +80,54 @@ describe &quot;problems/show.html.haml&quot; 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 &quot;problems/show.html.haml&quot; do @@ -94,8 +139,8 @@ describe &quot;problems/show.html.haml&quot; 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')
spec/views/problems/show.ics.haml_spec.rb 0 → 100644
@@ -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
spec/views/users/edit.html.haml_spec.rb 0 → 100644
@@ -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
spec/views/users/index.html.haml_spec.rb 0 → 100644
@@ -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
spec/views/users/new.html.haml_spec.rb 0 → 100644
@@ -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 &#39;users/show.html.haml&#39; do @@ -8,12 +9,12 @@ describe &#39;users/show.html.haml&#39; 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 &#39;users/show.html.haml&#39; do @@ -21,7 +22,6 @@ describe &#39;users/show.html.haml&#39; 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 &#39;users/show.html.haml&#39; do @@ -30,7 +30,6 @@ describe &#39;users/show.html.haml&#39; 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 &#39;users/show.html.haml&#39; do @@ -40,7 +39,6 @@ describe &#39;users/show.html.haml&#39; 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