Commit a0af55302055ad839e6bd2cd838b0f5c16092780

Authored by Andrey Subbota
2 parents 729b2c1b 30b7e5b6
Exists in master and in 1 other branch production

Merge branch 'errbit' into feature/upstream_merge

Showing 135 changed files with 571 additions and 292 deletions   Show diff stats
@@ -4,7 +4,6 @@ gem 'rails', '3.2.8' @@ -4,7 +4,6 @@ gem 'rails', '3.2.8'
4 gem 'mongoid', '~> 2.4.10' 4 gem 'mongoid', '~> 2.4.10'
5 gem 'mongoid_rails_migrations' 5 gem 'mongoid_rails_migrations'
6 gem 'devise', '~> 1.5.3' 6 gem 'devise', '~> 1.5.3'
7 -gem 'nokogiri'  
8 gem 'haml' 7 gem 'haml'
9 gem 'htmlentities', "~> 4.3.0" 8 gem 'htmlentities', "~> 4.3.0"
10 gem 'rack-ssl', :require => 'rack/ssl' # force SSL 9 gem 'rack-ssl', :require => 'rack/ssl' # force SSL
@@ -37,6 +36,8 @@ gem 'pivotal-tracker' @@ -37,6 +36,8 @@ gem 'pivotal-tracker'
37 gem 'ruby-fogbugz', :require => 'fogbugz' 36 gem 'ruby-fogbugz', :require => 'fogbugz'
38 # Github Issues 37 # Github Issues
39 gem 'octokit', '~> 1.0.0' 38 gem 'octokit', '~> 1.0.0'
  39 +# Gitlab
  40 +gem 'gitlab'
40 41
41 # Bitbucket Issues 42 # Bitbucket Issues
42 gem 'bitbucket_rest_api' 43 gem 'bitbucket_rest_api'
@@ -66,8 +67,6 @@ platform :ruby do @@ -66,8 +67,6 @@ platform :ruby do
66 gem 'bson_ext', '= 1.6.2' 67 gem 'bson_ext', '= 1.6.2'
67 end 68 end
68 69
69 -gem 'omniauth'  
70 -gem 'oa-core'  
71 gem 'ri_cal' 70 gem 'ri_cal'
72 gem 'yajl-ruby', :require => "yajl" 71 gem 'yajl-ruby', :require => "yajl"
73 72
@@ -77,25 +76,22 @@ group :development, :test do @@ -77,25 +76,22 @@ group :development, :test do
77 unless ENV["CI"] 76 unless ENV["CI"]
78 gem 'ruby-debug', :platform => :mri_18 77 gem 'ruby-debug', :platform => :mri_18
79 gem 'debugger', :platform => :mri_19 78 gem 'debugger', :platform => :mri_19
80 - gem 'pry'  
81 gem 'pry-rails' 79 gem 'pry-rails'
82 end 80 end
83 # gem 'rpm_contrib' 81 # gem 'rpm_contrib'
84 # gem 'newrelic_rpm' 82 # gem 'newrelic_rpm'
85 gem 'capistrano' 83 gem 'capistrano'
86 - gem 'capistrano_colors'  
87 end 84 end
88 85
89 group :test do 86 group :test do
90 gem 'capybara' 87 gem 'capybara'
91 gem 'launchy' 88 gem 'launchy'
92 - gem 'rspec', '~> 2.6'  
93 gem 'database_cleaner', '~> 0.6.0' 89 gem 'database_cleaner', '~> 0.6.0'
94 gem 'email_spec' 90 gem 'email_spec'
95 gem 'timecop' 91 gem 'timecop'
96 end 92 end
97 93
98 -group :heroku do 94 +group :heroku, :production do
99 gem 'unicorn' 95 gem 'unicorn'
100 end 96 end
101 97
@@ -108,6 +104,7 @@ group :assets do @@ -108,6 +104,7 @@ group :assets do
108 gem 'execjs' 104 gem 'execjs'
109 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows 105 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows
110 gem 'uglifier', '>= 1.0.3' 106 gem 'uglifier', '>= 1.0.3'
  107 + gem 'underscore-rails'
111 end 108 end
112 109
113 gem 'turbo-sprockets-rails3' 110 gem 'turbo-sprockets-rails3'
@@ -49,13 +49,12 @@ GEM @@ -49,13 +49,12 @@ GEM
49 builder (3.0.4) 49 builder (3.0.4)
50 campy (0.1.3) 50 campy (0.1.3)
51 multi_json (~> 1.0) 51 multi_json (~> 1.0)
52 - capistrano (2.13.4) 52 + capistrano (2.13.5)
53 highline 53 highline
54 net-scp (>= 1.0.0) 54 net-scp (>= 1.0.0)
55 net-sftp (>= 2.0.0) 55 net-sftp (>= 2.0.0)
56 net-ssh (>= 2.0.14) 56 net-ssh (>= 2.0.14)
57 net-ssh-gateway (>= 1.1.0) 57 net-ssh-gateway (>= 1.1.0)
58 - capistrano_colors (0.5.5)  
59 capybara (1.1.2) 58 capybara (1.1.2)
60 mime-types (>= 1.16) 59 mime-types (>= 1.16)
61 nokogiri (>= 1.3.3) 60 nokogiri (>= 1.3.3)
@@ -73,13 +72,13 @@ GEM @@ -73,13 +72,13 @@ GEM
73 rdoc 72 rdoc
74 daemons (1.1.8) 73 daemons (1.1.8)
75 database_cleaner (0.6.7) 74 database_cleaner (0.6.7)
76 - debugger (1.2.0) 75 + debugger (1.2.1)
77 columnize (>= 0.3.1) 76 columnize (>= 0.3.1)
78 debugger-linecache (~> 1.1.1) 77 debugger-linecache (~> 1.1.1)
79 - debugger-ruby_core_source (~> 1.1.3) 78 + debugger-ruby_core_source (~> 1.1.4)
80 debugger-linecache (1.1.2) 79 debugger-linecache (1.1.2)
81 debugger-ruby_core_source (>= 1.1.1) 80 debugger-ruby_core_source (>= 1.1.1)
82 - debugger-ruby_core_source (1.1.3) 81 + debugger-ruby_core_source (1.1.4)
83 devise (1.5.3) 82 devise (1.5.3)
84 bcrypt-ruby (~> 3.0) 83 bcrypt-ruby (~> 3.0)
85 orm_adapter (~> 0.0.3) 84 orm_adapter (~> 0.0.3)
@@ -169,7 +168,6 @@ GEM @@ -169,7 +168,6 @@ GEM
169 net-ssh-gateway (1.1.0) 168 net-ssh-gateway (1.1.0)
170 net-ssh (>= 1.99.1) 169 net-ssh (>= 1.99.1)
171 nokogiri (1.5.5) 170 nokogiri (1.5.5)
172 - oa-core (0.3.2)  
173 oauth2 (0.8.0) 171 oauth2 (0.8.0)
174 faraday (~> 0.8) 172 faraday (~> 0.8)
175 httpauth (~> 0.1) 173 httpauth (~> 0.1)
@@ -295,13 +293,14 @@ GEM @@ -295,13 +293,14 @@ GEM
295 treetop (1.4.10) 293 treetop (1.4.10)
296 polyglot 294 polyglot
297 polyglot (>= 0.3.1) 295 polyglot (>= 0.3.1)
298 - turbo-sprockets-rails3 (0.1.10)  
299 - railties (>= 3.1.0) 296 + turbo-sprockets-rails3 (0.2.12)
  297 + railties (>= 3.1.0, < 3.2.9)
300 sprockets (>= 2.0.0) 298 sprockets (>= 2.0.0)
301 tzinfo (0.3.33) 299 tzinfo (0.3.33)
302 uglifier (1.2.7) 300 uglifier (1.2.7)
303 execjs (>= 0.3.0) 301 execjs (>= 0.3.0)
304 multi_json (~> 1.3) 302 multi_json (~> 1.3)
  303 + underscore-rails (1.4.2.1)
305 unicorn (4.3.1) 304 unicorn (4.3.1)
306 kgio (~> 2.6) 305 kgio (~> 2.6)
307 rack 306 rack
@@ -328,7 +327,6 @@ DEPENDENCIES @@ -328,7 +327,6 @@ DEPENDENCIES
328 bson_ext (= 1.6.2) 327 bson_ext (= 1.6.2)
329 campy 328 campy
330 capistrano 329 capistrano
331 - capistrano_colors  
332 capybara 330 capybara
333 database_cleaner (~> 0.6.0) 331 database_cleaner (~> 0.6.0)
334 debugger 332 debugger
@@ -348,21 +346,16 @@ DEPENDENCIES @@ -348,21 +346,16 @@ DEPENDENCIES
348 mongo (= 1.6.2) 346 mongo (= 1.6.2)
349 mongoid (~> 2.4.10) 347 mongoid (~> 2.4.10)
350 mongoid_rails_migrations 348 mongoid_rails_migrations
351 - nokogiri  
352 - oa-core  
353 octokit (~> 1.0.0) 349 octokit (~> 1.0.0)
354 - omniauth  
355 omniauth-github 350 omniauth-github
356 oruen_redmine_client 351 oruen_redmine_client
357 pivotal-tracker 352 pivotal-tracker
358 - pry  
359 pry-rails 353 pry-rails
360 rack-ssl 354 rack-ssl
361 rack-ssl-enforcer 355 rack-ssl-enforcer
362 rails (= 3.2.8) 356 rails (= 3.2.8)
363 rails_autolink (~> 1.0.9) 357 rails_autolink (~> 1.0.9)
364 ri_cal 358 ri_cal
365 - rspec (~> 2.6)  
366 rspec-rails (~> 2.6) 359 rspec-rails (~> 2.6)
367 ruby-debug 360 ruby-debug
368 ruby-fogbugz 361 ruby-fogbugz
@@ -372,6 +365,7 @@ DEPENDENCIES @@ -372,6 +365,7 @@ DEPENDENCIES
372 timecop 365 timecop
373 turbo-sprockets-rails3 366 turbo-sprockets-rails3
374 uglifier (>= 1.0.3) 367 uglifier (>= 1.0.3)
  368 + underscore-rails
375 unicorn 369 unicorn
376 useragent (~> 0.3.1) 370 useragent (~> 0.3.1)
377 webmock 371 webmock
@@ -311,6 +311,7 @@ When upgrading Errbit, please run: @@ -311,6 +311,7 @@ When upgrading Errbit, please run:
311 311
312 ```bash 312 ```bash
313 git pull origin master # assuming origin is the github.com/errbit/errbit repo 313 git pull origin master # assuming origin is the github.com/errbit/errbit repo
  314 +bundle install
314 rake db:migrate 315 rake db:migrate
315 ``` 316 ```
316 317
@@ -376,12 +377,19 @@ card_type = Defect, status = Open, priority = Essential @@ -376,12 +377,19 @@ card_type = Defect, status = Open, priority = Essential
376 * For 'Account/Repository', the account will either be a username or organization. i.e. **errbit/errbit** 377 * For 'Account/Repository', the account will either be a username or organization. i.e. **errbit/errbit**
377 * You will also need to provide your username and password for your GitHub account. 378 * You will also need to provide your username and password for your GitHub account.
378 * (We'd really appreciate it if you wanted to help us implement OAuth instead!) 379 * (We'd really appreciate it if you wanted to help us implement OAuth instead!)
379 - 380 +
380 **Bitbucket Issues Integration** 381 **Bitbucket Issues Integration**
381 382
382 * For 'BITBUCKET REPO' field, the account will either be a username or organization. i.e. **errbit/errbit** 383 * For 'BITBUCKET REPO' field, the account will either be a username or organization. i.e. **errbit/errbit**
383 * You will also need to provide your username and password for your Bitbucket account. 384 * You will also need to provide your username and password for your Bitbucket account.
384 385
  386 +**Gitlab Issues Integration**
  387 +
  388 +* 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)
  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
  392 +
385 393
386 394
387 What if Errbit has an error? 395 What if Errbit has an error?
@@ -436,10 +444,15 @@ Special Thanks @@ -436,10 +444,15 @@ Special Thanks
436 -------------- 444 --------------
437 445
438 * [Michael Parenteau](http://michaelparenteau.com) - For rocking the Errbit design and providing a great user experience. 446 * [Michael Parenteau](http://michaelparenteau.com) - For rocking the Errbit design and providing a great user experience.
439 -* [Nick Recobra aka oruen](https://github.com/oruen) - Nick is Errbit's first core contributor. He's been working hard at making Errbit more awesome. 447 +* [Nick Recobra (@oruen)](https://github.com/oruen) - Nick is Errbit's first core contributor. He's been working hard at making Errbit more awesome.
  448 +* [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
  450 +* [Marcin Ciunelis (@martinciu)](https://github.com/martinciu) - Helping to improve Errbit's architecture
440 * [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. 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.
441 * [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](http://airbrakeapp.com). 452 * [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](http://airbrakeapp.com).
442 453
  454 +See the [contributors graph](https://github.com/errbit/errbit/graphs/contributors) for further details.
  455 +
443 456
444 Contributing 457 Contributing
445 ------------ 458 ------------
app/assets/images/alerts/help.gif

1.54 KB | W: | H:

1.53 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/alerts/important.gif

1.46 KB | W: | H:

1.45 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/alerts/info.gif

1.45 KB | W: | H:

1.44 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/alerts/title.gif

317 Bytes | W: | H:

309 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/bitbucket_create.png

2.54 KB | W: | H:

2.43 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/bitbucket_goto.png

3.61 KB | W: | H:

2.45 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/bitbucket_inactive.png

1.95 KB | W: | H:

1.23 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/campfire_create.png

3.19 KB | W: | H:

2.03 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/campfire_goto.png

3.19 KB | W: | H:

2.03 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/campfire_inactive.png

2.8 KB | W: | H:

1.58 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/fogbugz_create.png

4.78 KB | W: | H:

1.89 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/fogbugz_goto.png

4.78 KB | W: | H:

1.89 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/fogbugz_inactive.png

4.58 KB | W: | H:

1.04 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/github_create.png

2.3 KB | W: | H:

2.14 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/github_goto.png

2.3 KB | W: | H:

2.14 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/github_inactive.png

2.03 KB | W: | H:

1.05 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/gitlab_create.png 0 → 100644

4.47 KB

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

4.47 KB

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

4.35 KB

app/assets/images/gtalk_create.png

4.62 KB | W: | H:

1.71 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/gtalk_goto.png

4.62 KB | W: | H:

1.71 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/gtalk_inactive.png

4.07 KB | W: | H:

1.28 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hipchat_create.png

2.05 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hipchat_goto.png

2.05 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hipchat_inactive.png

1.16 KB | W: | H:

904 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hoiio_create.png

1.72 KB | W: | H:

1.52 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hoiio_goto.png

1.72 KB | W: | H:

1.52 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hoiio_inactive.png

874 Bytes | W: | H:

783 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/lighthouseapp_create.png

1.57 KB | W: | H:

1.38 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/lighthouseapp_goto.png

1.61 KB | W: | H:

1.4 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/lighthouseapp_inactive.png

1.57 KB | W: | H:

1.16 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/loader.gif

1.7 KB | W: | H:

1.6 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/mingle_create.png

2.41 KB | W: | H:

2.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/mingle_goto.png

2.41 KB | W: | H:

2.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/mingle_inactive.png

1.96 KB | W: | H:

1.38 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/none_create.png

799 Bytes | W: | H:

548 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/none_inactive.png

818 Bytes | W: | H:

558 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pivotal_create.png

1.5 KB | W: | H:

1.26 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pivotal_goto.png

1.52 KB | W: | H:

1.29 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pivotal_inactive.png

1.56 KB | W: | H:

1.1 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pushover_create.png

1.91 KB | W: | H:

1.56 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pushover_goto.png

1.91 KB | W: | H:

1.56 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pushover_inactive.png

1.02 KB | W: | H:

898 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/redmine_create.png

1.47 KB | W: | H:

1.33 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/redmine_goto.png

1.52 KB | W: | H:

1.35 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/redmine_inactive.png

1.52 KB | W: | H:

1.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/thumbs-up.png

1.42 KB | W: | H:

1.28 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/javascripts/application.js.erb
1 //= require jquery 1 //= require jquery
2 -//= require underscore-1.1.6 2 +//= require underscore
3 //= require rails 3 //= require rails
4 //= require form 4 //= require form
5 //= require jquery.pjax 5 //= require jquery.pjax
app/assets/javascripts/underscore-1.1.6.js
@@ -1,26 +0,0 @@ @@ -1,26 +0,0 @@
1 -// Underscore.js 1.1.6  
2 -// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.  
3 -// Underscore is freely distributable under the MIT license.  
4 -// Portions of Underscore are inspired or borrowed from Prototype,  
5 -// Oliver Steele's Functional, and John Resig's Micro-Templating.  
6 -// For all details and documentation:  
7 -// http://documentcloud.github.com/underscore  
8 -(function(){var p=this,C=p._,m={},i=Array.prototype,n=Object.prototype,f=i.slice,D=i.unshift,E=n.toString,l=n.hasOwnProperty,s=i.forEach,t=i.map,u=i.reduce,v=i.reduceRight,w=i.filter,x=i.every,y=i.some,o=i.indexOf,z=i.lastIndexOf;n=Array.isArray;var F=Object.keys,q=Function.prototype.bind,b=function(a){return new j(a)};typeof module!=="undefined"&&module.exports?(module.exports=b,b._=b):p._=b;b.VERSION="1.1.6";var h=b.each=b.forEach=function(a,c,d){if(a!=null)if(s&&a.forEach===s)a.forEach(c,d);else if(b.isNumber(a.length))for(var e=  
9 -0,k=a.length;e<k;e++){if(c.call(d,a[e],e,a)===m)break}else for(e in a)if(l.call(a,e)&&c.call(d,a[e],e,a)===m)break};b.map=function(a,c,b){var e=[];if(a==null)return e;if(t&&a.map===t)return a.map(c,b);h(a,function(a,g,G){e[e.length]=c.call(b,a,g,G)});return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var k=d!==void 0;a==null&&(a=[]);if(u&&a.reduce===u)return e&&(c=b.bind(c,e)),k?a.reduce(c,d):a.reduce(c);h(a,function(a,b,f){!k&&b===0?(d=a,k=!0):d=c.call(e,d,a,b,f)});if(!k)throw new TypeError("Reduce of empty array with no initial value");  
10 -return d};b.reduceRight=b.foldr=function(a,c,d,e){a==null&&(a=[]);if(v&&a.reduceRight===v)return e&&(c=b.bind(c,e)),d!==void 0?a.reduceRight(c,d):a.reduceRight(c);a=(b.isArray(a)?a.slice():b.toArray(a)).reverse();return b.reduce(a,c,d,e)};b.find=b.detect=function(a,c,b){var e;A(a,function(a,g,f){if(c.call(b,a,g,f))return e=a,!0});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(w&&a.filter===w)return a.filter(c,b);h(a,function(a,g,f){c.call(b,a,g,f)&&(e[e.length]=a)});return e};  
11 -b.reject=function(a,c,b){var e=[];if(a==null)return e;h(a,function(a,g,f){c.call(b,a,g,f)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=!0;if(a==null)return e;if(x&&a.every===x)return a.every(c,b);h(a,function(a,g,f){if(!(e=e&&c.call(b,a,g,f)))return m});return e};var A=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=!1;if(a==null)return e;if(y&&a.some===y)return a.some(c,d);h(a,function(a,b,f){if(e=c.call(d,a,b,f))return m});return e};b.include=b.contains=function(a,c){var b=  
12 -!1;if(a==null)return b;if(o&&a.indexOf===o)return a.indexOf(c)!=-1;A(a,function(a){if(b=a===c)return!0});return b};b.invoke=function(a,c){var d=f.call(arguments,2);return b.map(a,function(a){return(c.call?c||a:a[c]).apply(a,d)})};b.pluck=function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,  
13 -c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b<e.computed&&(e={value:a,computed:b})});return e.value};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,f){return{value:a,criteria:c.call(d,a,b,f)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),"value")};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=  
14 -function(a){if(!a)return[];if(a.toArray)return a.toArray();if(b.isArray(a))return a;if(b.isArguments(a))return f.call(a);return b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?f.call(a,0,b):a[0]};b.rest=b.tail=function(a,b,d){return f.call(a,b==null||d?1:b)};b.last=function(a){return a[a.length-1]};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a){return b.reduce(a,function(a,d){if(b.isArray(d))return a.concat(b.flatten(d));  
15 -a[a.length]=d;return a},[])};b.without=function(a){var c=f.call(arguments,1);return b.filter(a,function(a){return!b.include(c,a)})};b.uniq=b.unique=function(a,c){return b.reduce(a,function(a,e,f){if(0==f||(c===!0?b.last(a)!=e:!b.include(a,e)))a[a.length]=e;return a},[])};b.intersect=function(a){var c=f.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.zip=function(){for(var a=f.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),  
16 -e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(o&&a.indexOf===o)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);d=arguments[2]||1;for(var e=Math.max(Math.ceil((b-a)/  
17 -d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};b.bind=function(a,b){if(a.bind===q&&q)return q.apply(a,f.call(arguments,1));var d=f.call(arguments,2);return function(){return a.apply(b,d.concat(f.call(arguments)))}};b.bindAll=function(a){var c=f.call(arguments,1);c.length==0&&(c=b.functions(a));h(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var b=c.apply(this,arguments);return l.call(d,b)?d[b]:d[b]=a.apply(this,arguments)}};b.delay=  
18 -function(a,b){var d=f.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(f.call(arguments,1)))};var B=function(a,b,d){var e;return function(){var f=this,g=arguments,h=function(){e=null;a.apply(f,g)};d&&clearTimeout(e);if(d||!e)e=setTimeout(h,b)}};b.throttle=function(a,b){return B(a,b,!1)};b.debounce=function(a,b){return B(a,b,!0)};b.once=function(a){var b=!1,d;return function(){if(b)return d;b=!0;return d=a.apply(this,arguments)}};  
19 -b.wrap=function(a,b){return function(){var d=[a].concat(f.call(arguments));return b.apply(this,d)}};b.compose=function(){var a=f.call(arguments);return function(){for(var b=f.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=F||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,  
20 -b.identity)};b.functions=b.methods=function(a){return b.filter(b.keys(a),function(c){return b.isFunction(a[c])}).sort()};b.extend=function(a){h(f.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){h(f.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,c){if(a===c)return!0;var d=typeof a;if(d!=  
21 -typeof c)return!1;if(a==c)return!0;if(!a&&c||a&&!c)return!1;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual)return a.isEqual(c);if(b.isDate(a)&&b.isDate(c))return a.getTime()===c.getTime();if(b.isNaN(a)&&b.isNaN(c))return!1;if(b.isRegExp(a)&&b.isRegExp(c))return a.source===c.source&&a.global===c.global&&a.ignoreCase===c.ignoreCase&&a.multiline===c.multiline;if(d!=="object")return!1;if(a.length&&a.length!==c.length)return!1;d=b.keys(a);var e=b.keys(c);if(d.length!=e.length)return!1;  
22 -for(var f in a)if(!(f in c)||!b.isEqual(a[f],c[f]))return!1;return!0};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return!1;return!0};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=n||function(a){return E.call(a)==="[object Array]"};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};  
23 -b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===!0||a===!1};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===!1))};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){p._=C;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=  
24 -0;e<a;e++)b.call(d,e)};b.mixin=function(a){h(b.functions(a),function(c){H(c,b[c]=a[c])})};var I=0;b.uniqueId=function(a){var b=I++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||  
25 -null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return c?d(c):d};var j=function(a){this._wrapped=a};b.prototype=j.prototype;var r=function(a,c){return c?b(a).chain():a},H=function(a,c){j.prototype[a]=function(){var a=f.call(arguments);D.call(a,this._wrapped);return r(c.apply(b,a),this._chain)}};b.mixin(b);h(["pop","push","reverse","shift","sort",  
26 -"splice","unshift"],function(a){var b=i[a];j.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});h(["concat","join","slice"],function(a){var b=i[a];j.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});j.prototype.chain=function(){this._chain=!0;return this};j.prototype.value=function(){return this._wrapped}})();  
27 \ No newline at end of file 0 \ No newline at end of file
app/assets/stylesheets/errbit.css
@@ -586,8 +586,8 @@ div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover, @@ -586,8 +586,8 @@ div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover,
586 table.apps tbody tr:hover td ,table.errs tbody tr:hover td { background-color: #F2F2F2;} 586 table.apps tbody tr:hover td ,table.errs tbody tr:hover td { background-color: #F2F2F2;}
587 587
588 table.apps td.name, table.errs td.message { width: 100%; } 588 table.apps td.name, table.errs td.message { width: 100%; }
589 -table.apps td { padding: 16px 25px; }  
590 -table.apps th { padding: 10px 25px; } 589 +table.apps td { padding: 16px 20px; }
  590 +table.apps th { padding: 10px 20px; }
591 591
592 table.apps td.issue_tracker, table.apps td.count, table.apps td.deploy { 592 table.apps td.issue_tracker, table.apps td.count, table.apps td.deploy {
593 text-align: center; 593 text-align: center;
@@ -894,6 +894,11 @@ table.errs tr td.message .inline_comment em.commenter { @@ -894,6 +894,11 @@ table.errs tr td.message .inline_comment em.commenter {
894 color: #777; 894 color: #777;
895 } 895 }
896 896
  897 +textarea#comment_body {
  898 + width: 420px;
  899 + height: 80px;
  900 +}
  901 +
897 .current.asc:after { content: ' ↑'; } 902 .current.asc:after { content: ' ↑'; }
898 .current.desc:after { content: ' ↓'; } 903 .current.desc:after { content: ' ↓'; }
899 904
app/assets/stylesheets/images/button-bg.png

148 Bytes | W: | H:

108 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/content-fade.png

174 Bytes | W: | H:

134 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/error-badge-bg.png

119 Bytes | W: | H:

75 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/header.png

196 Bytes | W: | H:

109 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/add.png

1.04 KB | W: | H:

908 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/briefcase.png

675 Bytes | W: | H:

635 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/bullet-red-sm.png

417 Bytes | W: | H:

338 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/cross.png

473 Bytes | W: | H:

285 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/edit.png

1.22 KB | W: | H:

1.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/error.png

1.05 KB | W: | H:

993 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/github.png

1.94 KB | W: | H:

1.57 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/ical.png

1.86 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/notice.png

157 Bytes | W: | H:

112 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/required.png

250 Bytes | W: | H:

198 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/right-arrow.png

1.11 KB | W: | H:

847 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/success.png

1.06 KB | W: | H:

912 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/thumbs-up.png

1.42 KB | W: | H:

1.28 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/trash.png

1.65 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/unlink_github.png

2.02 KB | W: | H:

1.87 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/up.png

1.11 KB | W: | H:

837 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/user.png

877 Bytes | W: | H:

822 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/warning.png

674 Bytes | W: | H:

569 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/logo.png

3.21 KB | W: | H:

2.96 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/notebook.png

133 Bytes | W: | H:

91 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/resolved-badge-bg.png

119 Bytes | W: | H:

75 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/issue_tracker_icons.css.erb
@@ -3,16 +3,16 @@ @@ -3,16 +3,16 @@
3 3
4 <% trackers.each do |tracker| %> 4 <% trackers.each do |tracker| %>
5 div.issue_tracker.nested label.<%= tracker %> { 5 div.issue_tracker.nested label.<%= tracker %> {
6 - background: url(/assets/<%= tracker %>_inactive.png) no-repeat; 6 + background: url(<%= tracker %>_inactive.png) no-repeat;
7 } 7 }
8 div.issue_tracker.nested label.r_on.<%= tracker %> { 8 div.issue_tracker.nested label.r_on.<%= tracker %> {
9 - background: url(/assets/<%= tracker %>_create.png) no-repeat; 9 + background: url(<%= tracker %>_create.png) no-repeat;
10 } 10 }
11 #action-bar a.<%= tracker %>_create { 11 #action-bar a.<%= tracker %>_create {
12 - background: transparent url(/assets/<%= tracker %>_create.png) 6px 5px no-repeat; 12 + background: transparent url(<%= tracker %>_create.png) 6px 5px no-repeat;
13 } 13 }
14 #action-bar a.<%= tracker %>_goto { 14 #action-bar a.<%= tracker %>_goto {
15 - background: transparent url(/assets/<%= tracker %>_goto.png) 6px 5px no-repeat; 15 + background: transparent url(<%= tracker %>_goto.png) 6px 5px no-repeat;
16 } 16 }
17 <% end %> 17 <% end %>
18 18
app/assets/stylesheets/jquery.alerts.css
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 text-align: center; 17 text-align: center;
18 line-height: 1.75em; 18 line-height: 1.75em;
19 color: #666; 19 color: #666;
20 - background: #CCC url(/images/alerts/title.gif) top repeat-x; 20 + background: #CCC url(alerts/title.gif) top repeat-x;
21 border: solid 1px #FFF; 21 border: solid 1px #FFF;
22 border-bottom: solid 1px #999; 22 border-bottom: solid 1px #999;
23 cursor: default; 23 cursor: default;
@@ -26,21 +26,21 @@ @@ -26,21 +26,21 @@
26 } 26 }
27 27
28 #popup_content { 28 #popup_content {
29 - background: 16px 16px no-repeat url(/images/alerts/info.gif); 29 + background: 16px 16px no-repeat url(alerts/info.gif);
30 padding: 1em 1.75em; 30 padding: 1em 1.75em;
31 margin: 0em; 31 margin: 0em;
32 } 32 }
33 33
34 #popup_content.alert { 34 #popup_content.alert {
35 - background-image: url(/images/alerts/info.gif); 35 + background-image: url(alerts/info.gif);
36 } 36 }
37 37
38 #popup_content.confirm { 38 #popup_content.confirm {
39 - background-image: url(/images/alerts/important.gif); 39 + background-image: url(alerts/important.gif);
40 } 40 }
41 41
42 #popup_content.prompt { 42 #popup_content.prompt {
43 - background-image: url(/images/alerts/help.gif); 43 + background-image: url(alerts/help.gif);
44 } 44 }
45 45
46 #popup_message { 46 #popup_message {
app/assets/stylesheets/notification_service_icons.css.erb
@@ -3,16 +3,16 @@ @@ -3,16 +3,16 @@
3 3
4 <% notification_services.each do |notification_service| %> 4 <% notification_services.each do |notification_service| %>
5 div.notification_service.nested label.<%= notification_service %> { 5 div.notification_service.nested label.<%= notification_service %> {
6 - background: url(/assets/<%= notification_service %>_inactive.png) no-repeat; 6 + background: url(<%= notification_service %>_inactive.png) no-repeat;
7 } 7 }
8 div.notification_service.nested label.r_on.<%= notification_service %> { 8 div.notification_service.nested label.r_on.<%= notification_service %> {
9 - background: url(/assets/<%= notification_service %>_create.png) no-repeat; 9 + background: url(<%= notification_service %>_create.png) no-repeat;
10 } 10 }
11 #action-bar a.<%= notification_service %>_create { 11 #action-bar a.<%= notification_service %>_create {
12 - background: transparent url(/assets/<%= notification_service %>_create.png) 6px 5px no-repeat; 12 + background: transparent url(<%= notification_service %>_create.png) 6px 5px no-repeat;
13 } 13 }
14 #action-bar a.<%= notification_service %>_goto { 14 #action-bar a.<%= notification_service %>_goto {
15 - background: transparent url(/assets/<%= notification_service %>_goto.png) 6px 5px no-repeat; 15 + background: transparent url(<%= notification_service %>_goto.png) 6px 5px no-repeat;
16 } 16 }
17 <% end %> 17 <% end %>
18 18
app/controllers/apps_controller.rb
@@ -52,23 +52,7 @@ class AppsController &lt; InheritedResources::Base @@ -52,23 +52,7 @@ class AppsController &lt; InheritedResources::Base
52 52
53 protected 53 protected
54 def collection 54 def collection
55 - @unresolved_counts, @problem_counts = {}, {}  
56 - @apps ||= begin  
57 - apps = end_of_association_chain.all  
58 -  
59 - # Cache counts for unresolved errs and problems  
60 - apps.each do |app|  
61 - @unresolved_counts[app.id] ||= app.problems.unresolved.count  
62 - @problem_counts[app.id] ||= app.problems.count  
63 - end  
64 -  
65 - # Sort apps by number of unresolved errs, then problem counts.  
66 - apps.sort do |a,b|  
67 - (@unresolved_counts[b.id] <=> @unresolved_counts[a.id]).nonzero? ||  
68 - (@problem_counts[b.id] <=> @problem_counts[a.id]).nonzero? ||  
69 - a.name <=> b.name  
70 - end  
71 - end 55 + @apps ||= end_of_association_chain.all.sort
72 end 56 end
73 57
74 def initialize_subclassed_issue_tracker 58 def initialize_subclassed_issue_tracker
@@ -80,7 +64,7 @@ class AppsController &lt; InheritedResources::Base @@ -80,7 +64,7 @@ class AppsController &lt; InheritedResources::Base
80 end 64 end
81 end 65 end
82 66
83 - def initialize_subclassed_notification_service 67 + def initialize_subclassed_notification_service
84 # set the app's notification service 68 # set the app's notification service
85 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type] 69 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
86 if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type) 70 if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
app/controllers/problems_controller.rb
1 class ProblemsController < ApplicationController 1 class ProblemsController < ApplicationController
2 - include ActionView::Helpers::TextHelper  
3 -  
4 before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several] 2 before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
5 before_filter :find_problem, :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]
6 before_filter :find_selected_problems, :only => [: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]
@@ -37,36 +35,10 @@ class ProblemsController &lt; ApplicationController @@ -37,36 +35,10 @@ class ProblemsController &lt; ApplicationController
37 end 35 end
38 36
39 def create_issue 37 def create_issue
40 - # Create an issue on GitHub using user's github token  
41 - if params[:tracker] == 'user_github'  
42 - if !@app.github_repo?  
43 - flash[:error] = "This app doesn't have a GitHub repo set up."  
44 - elsif !current_user.github_account?  
45 - flash[:error] = "You haven't linked your Github account."  
46 - else  
47 - @tracker = GithubIssuesTracker.new(  
48 - :app => @app,  
49 - :username => current_user.github_login,  
50 - :oauth_token => current_user.github_oauth_token  
51 - )  
52 - end  
53 -  
54 - # Or, create an issue using the App's issue tracker  
55 - elsif @app.issue_tracker_configured?  
56 - @tracker = @app.issue_tracker 38 + issue_creation = IssueCreation.new(@problem, current_user, params[:tracker])
57 39
58 - # Otherwise, display error about missing tracker configuration.  
59 - else  
60 - flash[:error] = "This app has no issue tracker setup."  
61 - end  
62 -  
63 - if flash[:error].blank? && @tracker  
64 - begin  
65 - @tracker.create_issue @problem, current_user  
66 - rescue Exception => ex  
67 - Rails.logger.error "Error during issue creation: " << ex.message  
68 - flash[:error] = "There was an error during issue creation: #{ex.message}"  
69 - end 40 + unless issue_creation.execute
  41 + flash[:error] = issue_creation.errors[:base].first
70 end 42 end
71 43
72 redirect_to app_problem_path(@app, @problem) 44 redirect_to app_problem_path(@app, @problem)
@@ -87,13 +59,13 @@ class ProblemsController &lt; ApplicationController @@ -87,13 +59,13 @@ class ProblemsController &lt; ApplicationController
87 59
88 def resolve_several 60 def resolve_several
89 @selected_problems.each(&:resolve!) 61 @selected_problems.each(&:resolve!)
90 - flash[:success] = "Great news everyone! #{pluralize(@selected_problems.count, 'err has', 'errs have')} been resolved." 62 + flash[:success] = "Great news everyone! #{I18n.t(:n_errs_have, :count => @selected_problems.count)} been resolved."
91 redirect_to :back 63 redirect_to :back
92 end 64 end
93 65
94 def unresolve_several 66 def unresolve_several
95 @selected_problems.each(&:unresolve!) 67 @selected_problems.each(&:unresolve!)
96 - flash[:success] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been unresolved." 68 + flash[:success] = "#{I18n.t(:n_errs_have, :count => @selected_problems.count)} been unresolved."
97 redirect_to :back 69 redirect_to :back
98 end 70 end
99 71
@@ -109,13 +81,13 @@ class ProblemsController &lt; ApplicationController @@ -109,13 +81,13 @@ class ProblemsController &lt; ApplicationController
109 81
110 def unmerge_several 82 def unmerge_several
111 all = @selected_problems.map(&:unmerge!).flatten 83 all = @selected_problems.map(&:unmerge!).flatten
112 - flash[:success] = "#{pluralize(all.length, 'err has', 'errs have')} been unmerged." 84 + flash[:success] = "#{I18n.t(:n_errs_have, :count => all.length)} been unmerged."
113 redirect_to :back 85 redirect_to :back
114 end 86 end
115 87
116 def destroy_several 88 def destroy_several
117 nb_problem_destroy = ProblemDestroy.execute(@selected_problems) 89 nb_problem_destroy = ProblemDestroy.execute(@selected_problems)
118 - flash[:notice] = "#{pluralize(nb_problem_destroy, 'err has', 'errs have')} been deleted." 90 + flash[:notice] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted."
119 redirect_to :back 91 redirect_to :back
120 end 92 end
121 93
app/helpers/backtrace_line_helper.rb
@@ -19,13 +19,13 @@ module BacktraceLineHelper @@ -19,13 +19,13 @@ module BacktraceLineHelper
19 19
20 def link_to_github(line, text = nil) 20 def link_to_github(line, text = nil)
21 return unless line.app.github_repo? 21 return unless line.app.github_repo?
22 - href = "%s#L%s" % [line.app.github_url_to_file(line.file), line.number] 22 + href = "%s#L%s" % [line.app.github_url_to_file(line.decorated_path + line.file_name), line.number]
23 link_to(text || line.file_name, href, :target => '_blank') 23 link_to(text || line.file_name, href, :target => '_blank')
24 end 24 end
25 25
26 def link_to_bitbucket(line, text = nil) 26 def link_to_bitbucket(line, text = nil)
27 return unless line.app.bitbucket_repo? 27 return unless line.app.bitbucket_repo?
28 - href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.file), line.number] 28 + href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.decorated_path + line.file_name), line.number]
29 link_to(text || line.file_name, href, :target => '_blank') 29 link_to(text || line.file_name, href, :target => '_blank')
30 end 30 end
31 31
app/helpers/notices_helper.rb
1 # encoding: utf-8 1 # encoding: utf-8
2 module NoticesHelper 2 module NoticesHelper
3 def notice_atom_summary(notice) 3 def notice_atom_summary(notice)
4 - render "notices/atom_entry.html.haml", :notice => notice 4 + render "notices/atom_entry", :notice => notice
5 end 5 end
6 end 6 end
7 -  
app/interactors/issue_creation.rb 0 → 100644
@@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
  1 +class IssueCreation
  2 + include ActiveModel::Validations
  3 +
  4 + attr_reader :problem, :user, :tracker_name
  5 +
  6 + delegate :app, :to => :problem
  7 +
  8 + def initialize(problem, user, tracker_name)
  9 + @problem = problem
  10 + @user = user
  11 + @tracker_name = tracker_name
  12 + end
  13 +
  14 + def tracker
  15 + return @tracker if @tracker
  16 +
  17 + # Create an issue on GitHub using user's github token
  18 + if tracker_name == 'user_github'
  19 + if !app.github_repo?
  20 + errors.add :base, "This app doesn't have a GitHub repo set up."
  21 + elsif !user.github_account?
  22 + errors.add :base, "You haven't linked your Github account."
  23 + else
  24 + @tracker = GithubIssuesTracker.new(
  25 + :app => app,
  26 + :username => user.github_login,
  27 + :oauth_token => user.github_oauth_token
  28 + )
  29 + end
  30 +
  31 + # Or, create an issue using the App's issue tracker
  32 + elsif app.issue_tracker_configured?
  33 + @tracker = app.issue_tracker
  34 +
  35 + # Otherwise, display error about missing tracker configuration.
  36 + else
  37 + errors.add :base, "This app has no issue tracker setup."
  38 + end
  39 +
  40 + @tracker
  41 + end
  42 +
  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 +
  53 + errors.empty?
  54 + end
  55 +end
app/models/app.rb
1 class App 1 class App
2 include Mongoid::Document 2 include Mongoid::Document
3 include Mongoid::Timestamps 3 include Mongoid::Timestamps
  4 + include Comparable
4 5
5 field :name, :type => String 6 field :name, :type => String
6 field :api_key 7 field :api_key
@@ -98,12 +99,16 @@ class App @@ -98,12 +99,16 @@ class App
98 99
99 # Legacy apps don't have notify_on_errs and notify_on_deploys params 100 # Legacy apps don't have notify_on_errs and notify_on_deploys params
100 def notify_on_errs 101 def notify_on_errs
101 - !(self[:notify_on_errs] == false) 102 + !(super == false)
102 end 103 end
103 alias :notify_on_errs? :notify_on_errs 104 alias :notify_on_errs? :notify_on_errs
104 105
  106 + def notifiable?
  107 + notify_on_errs? && notification_recipients.any?
  108 + end
  109 +
105 def notify_on_deploys 110 def notify_on_deploys
106 - !(self[:notify_on_deploys] == false) 111 + !(super == false)
107 end 112 end
108 alias :notify_on_deploys? :notify_on_deploys 113 alias :notify_on_deploys? :notify_on_deploys
109 114
@@ -137,11 +142,11 @@ class App @@ -137,11 +142,11 @@ class App
137 142
138 143
139 def issue_tracker_configured? 144 def issue_tracker_configured?
140 - !!(issue_tracker && issue_tracker.class < IssueTracker && issue_tracker.project_id.present?) 145 + !!(issue_tracker.class < IssueTracker && issue_tracker.configured?)
141 end 146 end
142 147
143 def notification_service_configured? 148 def notification_service_configured?
144 - !!(notification_service && notification_service.class < NotificationService && notification_service.api_token.present?) 149 + !!(notification_service.class < NotificationService && notification_service.configured?)
145 end 150 end
146 151
147 152
@@ -169,6 +174,25 @@ class App @@ -169,6 +174,25 @@ class App
169 end 174 end
170 end 175 end
171 176
  177 + def unresolved_count
  178 + @unresolved_count ||= problems.unresolved.count
  179 + end
  180 +
  181 + def problem_count
  182 + @problem_count ||= problems.count
  183 + end
  184 +
  185 + # Compare by number of unresolved errs, then problem counts.
  186 + def <=>(other)
  187 + (other.unresolved_count <=> unresolved_count).nonzero? ||
  188 + (other.problem_count <=> problem_count).nonzero? ||
  189 + name <=> other.name
  190 + end
  191 +
  192 + def email_at_notices
  193 + Errbit::Config.per_app_email_at_notices ? super : Errbit::Config.email_at_notices
  194 + end
  195 +
172 protected 196 protected
173 197
174 def store_cached_attributes_on_problems 198 def store_cached_attributes_on_problems
app/models/issue_tracker.rb
@@ -35,5 +35,9 @@ class IssueTracker @@ -35,5 +35,9 @@ class IssueTracker
35 Label = '' 35 Label = ''
36 def self.label; self::Label; end 36 def self.label; self::Label; end
37 def label; self.class.label; end 37 def label; self.class.label; end
  38 +
  39 + def configured?
  40 + project_id.present?
  41 + end
38 end 42 end
39 43
app/models/issue_trackers/github_issues_tracker.rb
@@ -35,7 +35,7 @@ if defined? Octokit @@ -35,7 +35,7 @@ if defined? Octokit
35 end 35 end
36 36
37 begin 37 begin
38 - issue = client.create_issue(project_id, issue_title(problem), body_template.result(binding).unpack('C*').pack('U*'), options = {}) 38 + issue = client.create_issue(project_id, issue_title(problem), body_template.result(binding).unpack('C*').pack('U*'))
39 problem.update_attributes( 39 problem.update_attributes(
40 :issue_link => issue.html_url, 40 :issue_link => issue.html_url,
41 :issue_type => Label 41 :issue_type => Label
@@ -54,4 +54,4 @@ if defined? Octokit @@ -54,4 +54,4 @@ if defined? Octokit
54 "https://github.com/#{project_id}/issues" 54 "https://github.com/#{project_id}/issues"
55 end 55 end
56 end 56 end
57 -end  
58 \ No newline at end of file 57 \ No newline at end of file
  58 +end
app/models/issue_trackers/gitlab_tracker.rb 0 → 100644
@@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
  1 +if defined? Gitlab
  2 + class IssueTrackers::GitlabTracker < IssueTracker
  3 + Label = "gitlab"
  4 + Fields = [
  5 + [:account, {
  6 + :label => "Gitlab URL",
  7 + :placeholder => "e.g. https://example.net"
  8 + }],
  9 + [:api_token, {
  10 + :placeholder => "API Token for your account"
  11 + }],
  12 + [:project_id, {
  13 + :label => "Ticket Project Short Name / ID",
  14 + :placeholder => "Gitlab Project where issues will be created"
  15 + }]
  16 + ]
  17 +
  18 + def check_params
  19 + if Fields.detect {|f| self[f[0]].blank?}
  20 + errors.add :base, 'You must specify your Gitlab URL, API token and Project ID'
  21 + end
  22 + end
  23 +
  24 + def create_issue(problem, reported_by = nil)
  25 + Gitlab.configure do |config|
  26 + config.endpoint = "#{account}/api/v2"
  27 + config.private_token = api_token
  28 + config.user_agent = 'Errbit User Agent'
  29 + end
  30 + title = issue_title problem
  31 + description = body_template.result(binding)
  32 + Gitlab.create_issue(project_id, title, { :description => description, :labels => "errbit" } )
  33 + end
  34 +
  35 + def body_template
  36 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/gitlab_body.txt.erb").gsub(/^\s*/, ''))
  37 + end
  38 +
  39 + def url
  40 + "#{account}/#{project_id}/issues"
  41 + end
  42 + end
  43 +end
app/models/issue_trackers/lighthouse_tracker.rb
@@ -3,19 +3,22 @@ if defined? Lighthouse @@ -3,19 +3,22 @@ if defined? Lighthouse
3 Label = "lighthouseapp" 3 Label = "lighthouseapp"
4 Fields = [ 4 Fields = [
5 [:account, { 5 [:account, {
6 - :placeholder => "abc from http://abc.lighthouseapp.com" 6 + :label => "Subdomain",
  7 + :placeholder => "subdomain from http://{{subdomain}}.lighthouseapp.com"
7 }], 8 }],
8 [:api_token, { 9 [:api_token, {
9 - :placeholder => "API Token for your account" 10 + :label => "API Token",
  11 + :placeholder => "123456789abcdef123456789abcdef"
10 }], 12 }],
11 [:project_id, { 13 [:project_id, {
12 - :placeholder => "Lighthouse project" 14 + :label => "Project ID",
  15 + :placeholder => "123456"
13 }] 16 }]
14 ] 17 ]
15 18
16 def check_params 19 def check_params
17 if Fields.detect {|f| self[f[0]].blank? } 20 if Fields.detect {|f| self[f[0]].blank? }
18 - errors.add :base, 'You must specify your Lighthouseapp account, API token and Project ID' 21 + errors.add :base, 'You must specify your Lighthouseapp Subdomain, API token and Project ID'
19 end 22 end
20 end 23 end
21 24
app/models/issue_trackers/redmine_tracker.rb
@@ -4,7 +4,7 @@ if defined? RedmineClient @@ -4,7 +4,7 @@ if defined? RedmineClient
4 Fields = [ 4 Fields = [
5 [:account, { 5 [:account, {
6 :label => "Redmine URL", 6 :label => "Redmine URL",
7 - :placeholder => "e.g. http://www.redmine.org/" 7 + :placeholder => "http://www.redmine.org/"
8 }], 8 }],
9 [:api_token, { 9 [:api_token, {
10 :placeholder => "API Token for your account" 10 :placeholder => "API Token for your account"
app/models/notice.rb
@@ -13,7 +13,7 @@ class Notice @@ -13,7 +13,7 @@ class Notice
13 field :current_user, :type => Hash 13 field :current_user, :type => Hash
14 field :error_class 14 field :error_class
15 delegate :lines, :to => :backtrace, :prefix => true 15 delegate :lines, :to => :backtrace, :prefix => true
16 - delegate :app, :to => :err 16 + delegate :app, :problem, :to => :err
17 17
18 belongs_to :err 18 belongs_to :err
19 belongs_to :backtrace, :index => true 19 belongs_to :backtrace, :index => true
@@ -36,8 +36,6 @@ class Notice @@ -36,8 +36,6 @@ class Notice
36 scope :reverse_ordered, order_by(:created_at.desc) 36 scope :reverse_ordered, order_by(:created_at.desc)
37 scope :for_errs, lambda {|errs| where(:err_id.in => errs.all.map(&:id))} 37 scope :for_errs, lambda {|errs| where(:err_id.in => errs.all.map(&:id))}
38 38
39 - delegate :app, :problem, :to => :err  
40 -  
41 def user_agent 39 def user_agent
42 agent_string = env_vars['HTTP_USER_AGENT'] 40 agent_string = env_vars['HTTP_USER_AGENT']
43 agent_string.blank? ? nil : UserAgent.parse(agent_string) 41 agent_string.blank? ? nil : UserAgent.parse(agent_string)
@@ -66,7 +64,7 @@ class Notice @@ -66,7 +64,7 @@ class Notice
66 end 64 end
67 65
68 def request 66 def request
69 - read_attribute(:request) || {} 67 + super || {}
70 end 68 end
71 69
72 def url 70 def url
@@ -96,6 +94,18 @@ class Notice @@ -96,6 +94,18 @@ class Notice
96 backtrace_lines.in_app 94 backtrace_lines.in_app
97 end 95 end
98 96
  97 + def similar_count
  98 + problem.notices_count
  99 + end
  100 +
  101 + def notifiable?
  102 + app.email_at_notices.include?(similar_count)
  103 + end
  104 +
  105 + def should_notify?
  106 + app.notifiable? && notifiable?
  107 + end
  108 +
99 protected 109 protected
100 110
101 def increase_counter_cache 111 def increase_counter_cache
app/models/notice_observer.rb
@@ -7,16 +7,7 @@ class NoticeObserver &lt; Mongoid::Observer @@ -7,16 +7,7 @@ class NoticeObserver &lt; Mongoid::Observer
7 notice.app.notification_service.create_notification(notice.problem) 7 notice.app.notification_service.create_notification(notice.problem)
8 end 8 end
9 9
10 - if notice.app.notification_recipients.any?  
11 - Mailer.err_notification(notice).deliver  
12 - end 10 + Mailer.err_notification(notice).deliver if notice.should_notify?
13 end 11 end
14 12
15 - private  
16 -  
17 - def should_notify? notice  
18 - app = notice.app  
19 - app.notify_on_errs? and (app.notification_recipients.any? or !app.notification_service.nil?) and  
20 - (app.email_at_notices or Errbit::Config.email_at_notices).include?(notice.problem.notices_count)  
21 - end  
22 end 13 end
app/models/notification_service.rb
@@ -30,4 +30,8 @@ class NotificationService @@ -30,4 +30,8 @@ class NotificationService
30 Label = '' 30 Label = ''
31 def self.label; self::Label; end 31 def self.label; self::Label; end
32 def label; self.class.label; end 32 def label; self.class.label; end
  33 +
  34 + def configured?
  35 + api_token.present?
  36 + end
33 end 37 end
app/models/notification_services/campfire_service.rb
@@ -2,16 +2,18 @@ if defined? Campy @@ -2,16 +2,18 @@ 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, {  
6 - :placeholder => "Campfire Subdomain"  
7 - }],  
8 - [:api_token, {  
9 - :placeholder => "API Token"  
10 - }],  
11 - [:room_id, {  
12 - :placeholder => "Room ID",  
13 - :label => "Room ID"  
14 - }], 5 + [:subdomain, {
  6 + :label => "Subdomain",
  7 + :placeholder => "subdomain from http://{{subdomain}}.campfirenow.com"
  8 + }],
  9 + [:api_token, {
  10 + :label => "API Token",
  11 + :placeholder => "123456789abcdef123456789abcdef"
  12 + }],
  13 + [:room_id, {
  14 + :label => "Room ID",
  15 + :placeholder => "123456"
  16 + }]
15 ] 17 ]
16 18
17 def check_params 19 def check_params
@@ -21,7 +23,7 @@ if defined? Campy @@ -21,7 +23,7 @@ if defined? Campy
21 end 23 end
22 24
23 def url 25 def url
24 - "http://campfirenow.com/" 26 + "http://#{subdomain}.campfirenow.com/"
25 end 27 end
26 28
27 def create_notification(problem) 29 def create_notification(problem)
@@ -31,4 +33,4 @@ if defined? Campy @@ -31,4 +33,4 @@ if defined? Campy
31 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}"
32 end 34 end
33 end 35 end
34 -end 36 -end
  37 +end
35 \ No newline at end of file 38 \ No newline at end of file
app/models/notification_services/hipchat_service.rb
@@ -6,8 +6,8 @@ if defined? HipChat @@ -6,8 +6,8 @@ if defined? HipChat
6 :placeholder => "API Token" 6 :placeholder => "API Token"
7 }], 7 }],
8 [:room_id, { 8 [:room_id, {
9 - :placeholder => "Room ID",  
10 - :label => "Room ID" 9 + :placeholder => "Room name",
  10 + :label => "Room name"
11 }], 11 }],
12 ] 12 ]
13 13
@@ -17,6 +17,10 @@ if defined? HipChat @@ -17,6 +17,10 @@ if defined? HipChat
17 end 17 end
18 end 18 end
19 19
  20 + def url
  21 + "https://www.hipchat.com/sign_in"
  22 + end
  23 +
20 def create_notification(problem) 24 def create_notification(problem)
21 url = app_problem_url problem.app, problem 25 url = app_problem_url problem.app, problem
22 message = <<-MSG.strip_heredoc 26 message = <<-MSG.strip_heredoc
@@ -28,4 +32,4 @@ if defined? HipChat @@ -28,4 +32,4 @@ if defined? HipChat
28 client[room_id].send('Errbit', message, :color => 'red') 32 client[room_id].send('Errbit', message, :color => 'red')
29 end 33 end
30 end 34 end
31 -end  
32 \ No newline at end of file 35 \ No newline at end of file
  36 +end
app/models/notification_services/hoiio_service.rb
@@ -21,6 +21,10 @@ class NotificationServices::HoiioService &lt; NotificationService @@ -21,6 +21,10 @@ class NotificationServices::HoiioService &lt; NotificationService
21 end 21 end
22 end 22 end
23 23
  24 + def url
  25 + "https://secure.hoiio.com/user/"
  26 + end
  27 +
24 def notification_description(problem) 28 def notification_description(problem)
25 "[#{ problem.environment }]#{problem.message.to_s.truncate(50)}" 29 "[#{ problem.environment }]#{problem.message.to_s.truncate(50)}"
26 end 30 end
app/models/notification_services/pushover_service.rb
@@ -17,6 +17,10 @@ class NotificationServices::PushoverService &lt; NotificationService @@ -17,6 +17,10 @@ class NotificationServices::PushoverService &lt; NotificationService
17 end 17 end
18 end 18 end
19 19
  20 + def url
  21 + "https://pushover.net/login"
  22 + end
  23 +
20 def create_notification(problem) 24 def create_notification(problem)
21 # build the hoi client 25 # build the hoi client
22 notification = Rushover::Client.new(subdomain) 26 notification = Rushover::Client.new(subdomain)
app/models/problem.rb
@@ -57,6 +57,10 @@ class Problem @@ -57,6 +57,10 @@ class Problem
57 Notice.for_errs(errs).ordered 57 Notice.for_errs(errs).ordered
58 end 58 end
59 59
  60 + def comments_allowed?
  61 + Errbit::Config.allow_comments_with_issue_tracker || !app.issue_tracker_configured?
  62 + end
  63 +
60 def resolve! 64 def resolve!
61 self.update_attributes!(:resolved => true, :resolved_at => Time.now) 65 self.update_attributes!(:resolved => true, :resolved_at => Time.now)
62 end 66 end
app/models/user.rb
@@ -33,7 +33,7 @@ class User @@ -33,7 +33,7 @@ class User
33 end 33 end
34 34
35 def per_page 35 def per_page
36 - self[:per_page] || PER_PAGE 36 + super || PER_PAGE
37 end 37 end
38 38
39 def watching?(app) 39 def watching?(app)
app/views/apps/index.html.haml
@@ -47,8 +47,8 @@ @@ -47,8 +47,8 @@
47 - revision = app.deploys.last.short_revision 47 - revision = app.deploys.last.short_revision
48 = link_to( app.last_deploy_at.to_s(:micro) << (revision.present? ? " (#{revision})" : ""), app_deploys_path(app)) 48 = link_to( app.last_deploy_at.to_s(:micro) << (revision.present? ? " (#{revision})" : ""), app_deploys_path(app))
49 %td.count 49 %td.count
50 - - if @problem_counts[app.id] > 0  
51 - - unresolved = @unresolved_counts[app.id] 50 + - if app.problem_count > 0
  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
app/views/issue_trackers/gitlab_body.txt.erb 0 → 100644
@@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
  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 +
  18 +## Params ##
  19 +```
  20 +<%= pretty_hash(notice.params) %>
  21 +```
  22 +
  23 +## Session ##
  24 +```
  25 +<%= pretty_hash(notice.session) %>
  26 +```
  27 +
  28 +## Backtrace ##
  29 +```
  30 +<% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
  31 +<% end %>
  32 +```
  33 +
  34 +## Environment ##
  35 +
  36 +<table>
  37 +<% for key, val in notice.env_vars %>
  38 + <tr>
  39 + <td><%= key %>:</td>
  40 + <td><%= val %></td>
  41 + </tr>
  42 +<% end %>
  43 +</table>
  44 +<% end %>
  45 +
app/views/layouts/application.html.haml
@@ -30,7 +30,5 @@ @@ -30,7 +30,5 @@
30 - if content_for?(:comments) 30 - if content_for?(:comments)
31 #content-comments 31 #content-comments
32 = yield :comments 32 = yield :comments
33 - #footer= "Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.".html_safe 33 + #footer Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.
34 = yield :scripts 34 = yield :scripts
35 -  
36 -= yield :before_title  
37 \ No newline at end of file 35 \ No newline at end of file
app/views/notices/_atom_entry.atom.haml 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +%h2= notice.message
  2 +%h3 Summary
  3 +- if notice.request['url'].present?
  4 + %p
  5 + %strong URL:
  6 + = link_to(notice.request['url'], notice.request['url'])
  7 +%p
  8 + %strong Where:
  9 + = notice.where
  10 +%p
  11 + %strong Occured:
  12 + = notice.created_at.to_s(:micro)
  13 +%p
  14 + %strong Similar:
  15 + = notice.problem.notices_count - 1
  16 +
  17 +%h3 Params
  18 +%p= pretty_hash(notice.params)
  19 +
  20 +%h3 Session
  21 +%p= pretty_hash(notice.session)
  22 +
  23 +%h3 Backtrace
  24 +%table
  25 + - for line in notice.backtrace_lines
  26 + %tr
  27 + %td
  28 + = "#{line.number}:"
  29 + &nbsp;&nbsp;
  30 + %td
  31 + = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}"
  32 +
  33 +%h3 Environment
  34 +%table
  35 + - for key, val in notice.env_vars
  36 + %tr
  37 + %td
  38 + = h key
  39 + %td
  40 + = h val
  41 +
app/views/notices/_atom_entry.html.haml
@@ -1,41 +0,0 @@ @@ -1,41 +0,0 @@
1 -%h2= notice.message  
2 -%h3 Summary  
3 -- if notice.request['url'].present?  
4 - %p  
5 - %strong URL:  
6 - = link_to(notice.request['url'], notice.request['url'])  
7 -%p  
8 - %strong Where:  
9 - = notice.where  
10 -%p  
11 - %strong Occured:  
12 - = notice.created_at.to_s(:micro)  
13 -%p  
14 - %strong Similar:  
15 - = notice.problem.notices_count - 1  
16 -  
17 -%h3 Params  
18 -%p= pretty_hash(notice.params)  
19 -  
20 -%h3 Session  
21 -%p= pretty_hash(notice.session)  
22 -  
23 -%h3 Backtrace  
24 -%table  
25 - - for line in notice.backtrace_lines  
26 - %tr  
27 - %td  
28 - = "#{line.number}:"  
29 - &nbsp;&nbsp;  
30 - %td  
31 - = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}"  
32 -  
33 -%h3 Environment  
34 -%table  
35 - - for key, val in notice.env_vars  
36 - %tr  
37 - %td  
38 - = h key  
39 - %td  
40 - = h val  
41 -  
app/views/problems/show.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
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_path(@app) 6 + = link_to @app.name, @app
7 %strong Where: 7 %strong Where:
8 = @problem.where 8 = @problem.where
9 %br 9 %br
@@ -13,14 +13,14 @@ @@ -13,14 +13,14 @@
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? 15 - if @problem.unresolved?
16 - %span= link_to 'resolve', resolve_app_problem_path(@app, @problem), :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve' 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', app_problem_path(:app_id => @app.id, :id => @problem.id, :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link" 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' 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 Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? || @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|
@@ -33,18 +33,17 @@ @@ -33,18 +33,17 @@
33 = gravatar_tag comment.user.email, :s => 24 33 = gravatar_tag comment.user.email, :s => 24
34 %span.comment-info 34 %span.comment-info
35 = time_ago_in_words(comment.created_at, true) << " ago by " 35 = time_ago_in_words(comment.created_at, true) << " ago by "
36 - = link_to comment.user.email, user_path(comment.user)  
37 - %span.delete= link_to '&#10008;'.html_safe, app_problem_comment_path(@app, @problem, comment), :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment" 36 + = link_to comment.user.email, comment.user
38 - else 37 - else
39 %span.comment-info 38 %span.comment-info
40 = 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]"
41 - %span.delete= link_to '&#10008;'.html_safe, app_problem_comment_path(@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 sure you don't need this comment?" }, :class => "destroy-comment"
42 %tr 41 %tr
43 - %td= comment.body.gsub("\n", "<br>").html_safe  
44 - - if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured?  
45 - = form_for @comment, :url => app_problem_comments_path(@app, @problem) do |comment_form| 42 + %td= simple_format comment.body
  43 + - if @problem.comments_allowed?
  44 + = form_for [@app, @problem, @comment] do |comment_form|
46 %p Add a comment 45 %p Add a comment
47 - = comment_form.text_area :body, :style => "width: 420px; height: 80px;" 46 + = comment_form.text_area :body
48 = comment_form.submit "Save Comment" 47 = comment_form.submit "Save Comment"
49 48
50 %h4= @notice.try(:message) 49 %h4= @notice.try(:message)
config/application.rb
@@ -9,9 +9,12 @@ require &quot;action_mailer/railtie&quot; @@ -9,9 +9,12 @@ require &quot;action_mailer/railtie&quot;
9 require 'mongoid/railtie' 9 require 'mongoid/railtie'
10 require "sprockets/railtie" 10 require "sprockets/railtie"
11 11
12 -# If you have a Gemfile, require the gems listed there, including any gems  
13 -# you've limited to :test, :development, or :production.  
14 -Bundler.require(:default, Rails.env) if defined?(Bundler) 12 +if defined?(Bundler)
  13 + # If you precompile assets before deploying to production, use this line
  14 + Bundler.require(*Rails.groups(:assets => %w(development test)))
  15 + # If you want your assets lazily compiled in production, use this line
  16 + # Bundler.require(:default, :assets, Rails.env)
  17 +end
15 18
16 module Errbit 19 module Errbit
17 class Application < Rails::Application 20 class Application < Rails::Application
config/config.example.yml
@@ -53,6 +53,8 @@ deployment: @@ -53,6 +53,8 @@ deployment:
53 repository: http://github.com/errbit/errbit.git 53 repository: http://github.com/errbit/errbit.git
54 user: deploy 54 user: deploy
55 deploy_to: /var/www/apps/errbit 55 deploy_to: /var/www/apps/errbit
  56 + # setup path to unicorn pids folder (or deploy_to/shared/pids will be used)
  57 + # pids: /var/www/apps/errbit/shared/pids
56 58
57 # GitHub OAuth configuration 59 # GitHub OAuth configuration
58 # If you want to allow authentication via GitHub, you will need to register 60 # If you want to allow authentication via GitHub, you will need to register
config/deploy.example.rb
@@ -39,6 +39,8 @@ set(:current_branch) { `git branch`.match(/\* (\S+)\s/m)[1] || raise(&quot;Couldn&#39;t d @@ -39,6 +39,8 @@ set(:current_branch) { `git branch`.match(/\* (\S+)\s/m)[1] || raise(&quot;Couldn&#39;t d
39 set :branch, defer { current_branch } 39 set :branch, defer { current_branch }
40 40
41 after 'deploy:update_code', 'errbit:symlink_configs' 41 after 'deploy:update_code', 'errbit:symlink_configs'
  42 +# 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 44
43 namespace :deploy do 45 namespace :deploy do
44 task :start do ; end 46 task :start do ; end
@@ -72,3 +74,24 @@ namespace :db do @@ -72,3 +74,24 @@ namespace :db do
72 end 74 end
73 end 75 end
74 76
  77 +namespace :unicorn do
  78 + set(:unicorn_pid) do
  79 + path = config['pids'] || "#{deploy_to}/shared/pids"
  80 + "`cat #{path}/unicorn.pid`"
  81 + end
  82 +
  83 + desc 'Reload unicorn'
  84 + task :reload, :roles => :app, :except => { :no_release => true } do
  85 + run "kill -HUP #{unicorn_pid}"
  86 + end
  87 +
  88 + desc 'Stop unicorn'
  89 + task :stop, :roles => :app, :except => { :no_release => true } do
  90 + run "kill -QUIT #{unicorn_pid}"
  91 + end
  92 +
  93 + desc 'Reexecute unicorn'
  94 + task :reexec, :roles => :app, :except => { :no_release => true } do
  95 + run "kill -USR2 #{unicorn_pid}"
  96 + end
  97 +end
config/locales/en.yml
@@ -11,3 +11,6 @@ en: @@ -11,3 +11,6 @@ en:
11 success: "Good news everyone! '%{app_name}' was successfully updated." 11 success: "Good news everyone! '%{app_name}' was successfully updated."
12 destroy: 12 destroy:
13 success: "'%{app_name}' was successfully destroyed." 13 success: "'%{app_name}' was successfully destroyed."
  14 + n_errs_have:
  15 + one: "%{count} err has"
  16 + other: "%{count} errs have"
config/mongoid.example.yml
@@ -30,4 +30,4 @@ production: @@ -30,4 +30,4 @@ production:
30 port: <%= ENV['MONGOID_PORT'] %> 30 port: <%= ENV['MONGOID_PORT'] %>
31 username: <%= ENV['MONGOID_USERNAME'] %> 31 username: <%= ENV['MONGOID_USERNAME'] %>
32 password: <%= ENV['MONGOID_PASSWORD'] %> 32 password: <%= ENV['MONGOID_PASSWORD'] %>
33 - database: <%= ENV['MONGOID_DATABASE'] %>  
34 \ No newline at end of file 33 \ No newline at end of file
  34 + database: <%= ENV['MONGOID_DATABASE'] %>
config/newrelic.example.yml
@@ -31,13 +31,13 @@ common: &amp;default_settings @@ -31,13 +31,13 @@ common: &amp;default_settings
31 # into a New Relic "application" on your home dashboard page. If you want 31 # into a New Relic "application" on your home dashboard page. If you want
32 # to map this instance into multiple apps, like "AJAX Requests" and 32 # to map this instance into multiple apps, like "AJAX Requests" and
33 # "All UI" then specify a semicolon-separated list of up to three 33 # "All UI" then specify a semicolon-separated list of up to three
34 - # distinct names. If you comment this out, it defaults to the 34 + # distinct names. If you comment this out, it defaults to the
35 # capitalized RAILS_ENV (i.e., Production, Staging, etc) 35 # capitalized RAILS_ENV (i.e., Production, Staging, etc)
36 app_name: <%= ENV["NEW_RELIC_APP_NAME"] %> 36 app_name: <%= ENV["NEW_RELIC_APP_NAME"] %>
37 37
38 - # When "true", the agent collects performance data about your 38 + # When "true", the agent collects performance data about your
39 # application and reports this data to the New Relic service at 39 # application and reports this data to the New Relic service at
40 - # newrelic.com. This global switch is normally overridden for each 40 + # newrelic.com. This global switch is normally overridden for each
41 # environment below. (formerly called 'enabled') 41 # environment below. (formerly called 'enabled')
42 monitor_mode: true 42 monitor_mode: true
43 43
@@ -72,11 +72,11 @@ common: &amp;default_settings @@ -72,11 +72,11 @@ common: &amp;default_settings
72 72
73 # Set your application's Apdex threshold value with the 'apdex_t' 73 # Set your application's Apdex threshold value with the 'apdex_t'
74 # setting, in seconds. The apdex_t value determines the buckets used 74 # setting, in seconds. The apdex_t value determines the buckets used
75 - # to compute your overall Apdex score. 75 + # to compute your overall Apdex score.
76 # Requests that take less than apdex_t seconds to process will be 76 # Requests that take less than apdex_t seconds to process will be
77 # classified as Satisfying transactions; more than apdex_t seconds 77 # classified as Satisfying transactions; more than apdex_t seconds
78 # as Tolerating transactions; and more than four times the apdex_t 78 # as Tolerating transactions; and more than four times the apdex_t
79 - # value as Frustrating transactions. 79 + # value as Frustrating transactions.
80 # For more about the Apdex standard, see 80 # For more about the Apdex standard, see
81 # http://support.newrelic.com/faqs/general/apdex 81 # http://support.newrelic.com/faqs/general/apdex
82 apdex_t: 0.5 82 apdex_t: 0.5
@@ -97,7 +97,7 @@ common: &amp;default_settings @@ -97,7 +97,7 @@ common: &amp;default_settings
97 # Rails: the RoR filter_parameter_logging excludes parameters 97 # Rails: the RoR filter_parameter_logging excludes parameters
98 # Java: create a config setting called "ignored_params" and set it to 98 # Java: create a config setting called "ignored_params" and set it to
99 # a comma separated list of HTTP parameter names. 99 # a comma separated list of HTTP parameter names.
100 - # ex: ignored_params: credit_card, ssn, password 100 + # ex: ignored_params: credit_card, ssn, password
101 capture_params: false 101 capture_params: false
102 102
103 # Transaction tracer captures deep information about slow 103 # Transaction tracer captures deep information about slow
@@ -136,7 +136,7 @@ common: &amp;default_settings @@ -136,7 +136,7 @@ common: &amp;default_settings
136 # set to false when using other adapters. 136 # set to false when using other adapters.
137 # explain_enabled: true 137 # explain_enabled: true
138 138
139 - # Threshold for query execution time below which query plans will not 139 + # Threshold for query execution time below which query plans will not
140 # not be captured. Relevant only when `explain_enabled` is true. 140 # not be captured. Relevant only when `explain_enabled` is true.
141 # explain_threshold: 0.5 141 # explain_threshold: 0.5
142 142
@@ -149,10 +149,10 @@ common: &amp;default_settings @@ -149,10 +149,10 @@ common: &amp;default_settings
149 # product level. 149 # product level.
150 enabled: true 150 enabled: true
151 151
152 - # Rails Only - tells error collector whether or not to capture a  
153 - # source snippet around the place of the error when errors are View 152 + # Rails Only - tells error collector whether or not to capture a
  153 + # source snippet around the place of the error when errors are View
154 # related. 154 # related.
155 - capture_source: true 155 + capture_source: true
156 156
157 # To stop specific errors from reporting to New Relic, set this property 157 # To stop specific errors from reporting to New Relic, set this property
158 # to comma-separated values. Default is to ignore routing errors, 158 # to comma-separated values. Default is to ignore routing errors,
@@ -172,7 +172,7 @@ common: &amp;default_settings @@ -172,7 +172,7 @@ common: &amp;default_settings
172 # disable_memcache_instrumentation: true 172 # disable_memcache_instrumentation: true
173 # disable_dj: true 173 # disable_dj: true
174 174
175 - # Certain types of instrumentation such as GC stats will not work if 175 + # Certain types of instrumentation such as GC stats will not work if
176 # you are running multi-threaded. Please let us know. 176 # you are running multi-threaded. Please let us know.
177 # multi_threaded = false 177 # multi_threaded = false
178 178
@@ -190,15 +190,15 @@ development: @@ -190,15 +190,15 @@ development:
190 <<: *default_settings 190 <<: *default_settings
191 # Turn off communication to New Relic service in development mode (also 191 # Turn off communication to New Relic service in development mode (also
192 # 'enabled'). 192 # 'enabled').
193 - # NOTE: for initial evaluation purposes, you may want to temporarily 193 + # NOTE: for initial evaluation purposes, you may want to temporarily
194 # turn agent communication on in development mode. 194 # turn agent communication on in development mode.
195 monitor_mode: false 195 monitor_mode: false
196 196
197 - # Rails Only - when running in Developer Mode, the New Relic Agent will 197 + # Rails Only - when running in Developer Mode, the New Relic Agent will
198 # present performance information on the last 100 transactions you have 198 # present performance information on the last 100 transactions you have
199 # executed since starting the app server. 199 # executed since starting the app server.
200 # NOTE: There is substantial overhead when running in developer mode. 200 # NOTE: There is substantial overhead when running in developer mode.
201 - # Do not use for production or load testing. 201 + # Do not use for production or load testing.
202 developer_mode: true 202 developer_mode: true
203 203
204 # Enable textmate links 204 # Enable textmate links
config/unicorn.rb
@@ -2,4 +2,4 @@ @@ -2,4 +2,4 @@
2 2
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  
6 \ No newline at end of file 5 \ No newline at end of file
  6 +preload_app true
db/migrate/20110422152027_move_notices_to_separate_collection.rb
@@ -5,10 +5,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration @@ -5,10 +5,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration
5 errs = mongo_db.collection("errs").find({ }, :fields => ["notices"]) 5 errs = mongo_db.collection("errs").find({ }, :fields => ["notices"])
6 errs.each do |err| 6 errs.each do |err|
7 next unless err['notices'] 7 next unless err['notices']
8 - 8 +
9 # This Err was created after the Problem->Err->Notice redesign 9 # This Err was created after the Problem->Err->Notice redesign
10 next if err['app_id'].nil? or err['problem_id'] 10 next if err['app_id'].nil? or err['problem_id']
11 - 11 +
12 e = Err.find(err['_id']) 12 e = Err.find(err['_id'])
13 # disable email notifications 13 # disable email notifications
14 old_notify = e.app.notify_on_errs? 14 old_notify = e.app.notify_on_errs?
db/migrate/20111019163257_add_problem_comments_count.rb
@@ -7,4 +7,4 @@ class AddProblemCommentsCount &lt; Mongoid::Migration @@ -7,4 +7,4 @@ class AddProblemCommentsCount &lt; Mongoid::Migration
7 7
8 def self.down 8 def self.down
9 end 9 end
10 -end  
11 \ No newline at end of file 10 \ No newline at end of file
  11 +end
db/migrate/20111102173347_cache_problem_statistics_fix.rb
@@ -30,4 +30,4 @@ class CacheProblemStatisticsFix &lt; Mongoid::Migration @@ -30,4 +30,4 @@ class CacheProblemStatisticsFix &lt; Mongoid::Migration
30 counter 30 counter
31 end 31 end
32 32
33 -end  
34 \ No newline at end of file 33 \ No newline at end of file
  34 +end
db/migrate/20120822195841_set_first_notice_at_on_problems.rb
@@ -7,4 +7,4 @@ class SetFirstNoticeAtOnProblems &lt; Mongoid::Migration @@ -7,4 +7,4 @@ class SetFirstNoticeAtOnProblems &lt; Mongoid::Migration
7 7
8 def self.down 8 def self.down
9 end 9 end
10 -end  
11 \ No newline at end of file 10 \ No newline at end of file
  11 +end
db/migrate/20120829034812_ensure_that_problems_last_notice_at_is_not_nil.rb
@@ -2,15 +2,15 @@ class EnsureThatProblemsLastNoticeAtIsNotNil &lt; Mongoid::Migration @@ -2,15 +2,15 @@ class EnsureThatProblemsLastNoticeAtIsNotNil &lt; Mongoid::Migration
2 def self.up 2 def self.up
3 Problem.where("$or" => [{:last_notice_at => nil}, {:first_notice_at => nil}]).each do |problem| 3 Problem.where("$or" => [{:last_notice_at => nil}, {:first_notice_at => nil}]).each do |problem|
4 first_notice = problem.notices.order_by([:created_at, :asc]).first 4 first_notice = problem.notices.order_by([:created_at, :asc]).first
5 - 5 +
6 # Destroy problems with no notices 6 # Destroy problems with no notices
7 if first_notice.nil? 7 if first_notice.nil?
8 problem.destroy 8 problem.destroy
9 next 9 next
10 end 10 end
11 - 11 +
12 last_notice = problem.notices.order_by([:created_at, :asc]).last 12 last_notice = problem.notices.order_by([:created_at, :asc]).last
13 - 13 +
14 problem.update_attributes!({ 14 problem.update_attributes!({
15 :first_notice_at => first_notice.created_at, 15 :first_notice_at => first_notice.created_at,
16 :last_notice_at => last_notice.created_at 16 :last_notice_at => last_notice.created_at
lib/tasks/errbit/bootstrap.rake
1 require 'fileutils' 1 require 'fileutils'
2 2
3 namespace :errbit do 3 namespace :errbit do
4 - 4 +
5 desc "Copys of example config files" 5 desc "Copys of example config files"
6 task :copy_configs do 6 task :copy_configs do
7 configs = { 7 configs = {
@@ -9,7 +9,7 @@ namespace :errbit do @@ -9,7 +9,7 @@ namespace :errbit do
9 'deploy.example.rb' => 'deploy.rb', 9 'deploy.example.rb' => 'deploy.rb',
10 (ENV['HEROKU'] ? 'mongoid.mongohq.yml' : 'mongoid.example.yml') => 'mongoid.yml' 10 (ENV['HEROKU'] ? 'mongoid.mongohq.yml' : 'mongoid.example.yml') => 'mongoid.yml'
11 } 11 }
12 - 12 +
13 puts "Copying example config files..." 13 puts "Copying example config files..."
14 configs.each do |old, new| 14 configs.each do |old, new|
15 if File.exists?("config/#{new}") 15 if File.exists?("config/#{new}")
@@ -20,7 +20,7 @@ namespace :errbit do @@ -20,7 +20,7 @@ namespace :errbit do
20 end 20 end
21 end 21 end
22 end 22 end
23 - 23 +
24 desc "Copy's over example files and seeds the database" 24 desc "Copy's over example files and seeds the database"
25 task :bootstrap do 25 task :bootstrap do
26 Rake::Task['errbit:copy_configs'].execute 26 Rake::Task['errbit:copy_configs'].execute
@@ -29,5 +29,5 @@ namespace :errbit do @@ -29,5 +29,5 @@ namespace :errbit do
29 puts "\n" 29 puts "\n"
30 Rake::Task['db:mongoid:create_indexes'].invoke 30 Rake::Task['db:mongoid:create_indexes'].invoke
31 end 31 end
32 -  
33 -end  
34 \ No newline at end of file 32 \ No newline at end of file
  33 +
  34 +end
spec/controllers/api/v1/problems_controller_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 -describe Api::V1::ProblemsController do  
4 - 3 +describe Api::V1::ProblemsController do
  4 +
5 context "when logged in" do 5 context "when logged in" do
6 before do 6 before do
7 @user = Fabricate(:user) 7 @user = Fabricate(:user)
8 end 8 end
9 - 9 +
10 describe "GET /api/v1/problems" do 10 describe "GET /api/v1/problems" do
11 before do 11 before do
12 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 01), :resolved_at => Date.new(2012, 8, 02)) 12 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 01), :resolved_at => Date.new(2012, 8, 02))
@@ -14,45 +14,45 @@ describe Api::V1::ProblemsController do @@ -14,45 +14,45 @@ describe Api::V1::ProblemsController do
14 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 21)) 14 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 21))
15 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 30)) 15 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 30))
16 end 16 end
17 -  
18 -  
19 - 17 +
  18 +
  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 lambda { JSON.load(response.body) }.should_not 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 lambda { XML::Parser.string(response.body).parse }.should_not raise_error
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 lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
33 end 33 end
34 -  
35 -  
36 - 34 +
  35 +
  36 +
37 describe "given a date range" do 37 describe "given a date range" do
38 - 38 +
39 it "should return only the problems open during the date range" do 39 it "should return only the problems open during the date range" do
40 get :index, {:auth_token => @user.authentication_token, :start_date => "2012-08-20", :end_date => "2012-08-27"} 40 get :index, {:auth_token => @user.authentication_token, :start_date => "2012-08-20", :end_date => "2012-08-27"}
41 response.should be_success 41 response.should be_success
42 problems = JSON.load response.body 42 problems = JSON.load response.body
43 problems.length.should == 2 43 problems.length.should == 2
44 end 44 end
45 - 45 +
46 end 46 end
47 - 47 +
48 it "should return all problems" do 48 it "should return all problems" do
49 get :index, {:auth_token => @user.authentication_token} 49 get :index, {:auth_token => @user.authentication_token}
50 response.should be_success 50 response.should be_success
51 problems = JSON.load response.body 51 problems = JSON.load response.body
52 problems.length.should == 4 52 problems.length.should == 4
53 end 53 end
54 - 54 +
55 end 55 end
56 end 56 end
57 - 57 +
58 end 58 end
spec/controllers/apps_controller_spec.rb
@@ -31,17 +31,6 @@ describe AppsController do @@ -31,17 +31,6 @@ describe AppsController do
31 assigns(:apps).should_not include(unwatched_app) 31 assigns(:apps).should_not include(unwatched_app)
32 end 32 end
33 end 33 end
34 -  
35 - context 'when there is only one app' do  
36 - it 'sets unresolved_counts and problem_counts variables' do  
37 - sign_in Fabricate(:admin)  
38 - app = Fabricate(:app)  
39 - get :index  
40 -  
41 - assigns(:unresolved_counts).should == {app.id => 0}  
42 - assigns(:problem_counts).should == {app.id => 0}  
43 - end  
44 - end  
45 end 34 end
46 35
47 describe "GET /apps/:id" do 36 describe "GET /apps/:id" do
@@ -261,6 +250,10 @@ describe AppsController do @@ -261,6 +250,10 @@ describe AppsController do
261 end 250 end
262 251
263 context "changing email_at_notices" do 252 context "changing email_at_notices" do
  253 + before do
  254 + Errbit::Config.per_app_email_at_notices = true
  255 + end
  256 +
264 it "should parse legal csv values" do 257 it "should parse legal csv values" do
265 put :update, :id => @app.id, :app => { :email_at_notices => '1, 4, 7,8, 10' } 258 put :update, :id => @app.id, :app => { :email_at_notices => '1, 4, 7,8, 10' }
266 @app.reload 259 @app.reload
spec/controllers/problems_controller_spec.rb
@@ -419,6 +419,16 @@ describe ProblemsController do @@ -419,6 +419,16 @@ describe ProblemsController do
419 post :resolve_several, :problems => [@problem2.id.to_s] 419 post :resolve_several, :problems => [@problem2.id.to_s]
420 @problem2.reload.resolved?.should == true 420 @problem2.reload.resolved?.should == true
421 end 421 end
  422 +
  423 + it "should display a message about 1 err" do
  424 + post :resolve_several, :problems => [@problem2.id.to_s]
  425 + flash[:success].should match(/1 err has been resolved/)
  426 + end
  427 +
  428 + it "should display a message about 2 errs" do
  429 + post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
  430 + flash[:success].should match(/2 errs have been resolved/)
  431 + end
422 end 432 end
423 433
424 context "POST /problems/unresolve_several" do 434 context "POST /problems/unresolve_several" do
spec/fabricators/issue_tracker_fabricator.rb
@@ -15,6 +15,10 @@ Fabricator :redmine_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;IssueTrack @@ -15,6 +15,10 @@ Fabricator :redmine_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;IssueTrack
15 account 'http://redmine.example.com' 15 account 'http://redmine.example.com'
16 end 16 end
17 17
  18 +Fabricator :gitlab_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::GitlabTracker" do
  19 + account 'http://gitlab.example.com'
  20 +end
  21 +
18 Fabricator :mingle_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::MingleTracker" do 22 Fabricator :mingle_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::MingleTracker" do
19 account 'https://mingle.example.com' 23 account 'https://mingle.example.com'
20 ticket_properties 'card_type = Defect, defect_status = open, priority = essential' 24 ticket_properties 'card_type = Defect, defect_status = open, priority = essential'
spec/fixtures/hoptoad_test_notice_with_one_line_of_backtrace.xml
@@ -72,4 +72,4 @@ @@ -72,4 +72,4 @@
72 <project-root>/path/to/sample/project</project-root> 72 <project-root>/path/to/sample/project</project-root>
73 <environment-name>development</environment-name> 73 <environment-name>development</environment-name>
74 </server-environment> 74 </server-environment>
75 -</notice>  
76 \ No newline at end of file 75 \ No newline at end of file
  76 +</notice>
spec/interactors/issue_creation_spec.rb 0 → 100644
@@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
  1 +require 'spec_helper'
  2 +
  3 +describe IssueCreation do
  4 + subject(:issue_creation) { IssueCreation.new(problem, user, tracker_name) }
  5 +
  6 + let(:problem) { notice.problem }
  7 + let(:notice) { Fabricate(:notice) }
  8 + let(:user) { Fabricate(:admin) }
  9 + let(:errors) { issue_creation.errors[:base] }
  10 + let(:tracker_name) { nil }
  11 +
  12 + it "adds the error when issue tracker isn't configured" do
  13 + issue_creation.execute
  14 + expect(errors).to include("This app has no issue tracker setup.")
  15 + end
  16 +
  17 + it 'creates an issue if issue tracker is configured' do
  18 + tracker = Fabricate(:lighthouse_tracker, :app => notice.app)
  19 + tracker.should_receive(:create_issue)
  20 + issue_creation.execute
  21 + expect(errors).to be_empty
  22 + end
  23 +
  24 + context "with user's github" do
  25 + let(:tracker_name) { 'user_github' }
  26 +
  27 + it "adds the error when repo isn't set up" do
  28 + issue_creation.execute
  29 + expect(errors).to include("This app doesn't have a GitHub repo set up.")
  30 + end
  31 +
  32 + context 'with repo set up' do
  33 + before do
  34 + notice.app.update_attribute(:github_repo, 'errbit/errbit')
  35 + end
  36 +
  37 + it "adds the error when github account isn't linked" do
  38 + issue_creation.execute
  39 + expect(errors).to include("You haven't linked your Github account.")
  40 + end
  41 +
  42 + it 'creates an issue if github account is linked' do
  43 + user.github_login = 'admin'
  44 + user.github_oauth_token = 'oauthtoken'
  45 + user.save!
  46 +
  47 + GithubIssuesTracker.any_instance.should_receive(:create_issue)
  48 + issue_creation.execute
  49 + expect(errors).to be_empty
  50 + end
  51 + end
  52 + end
  53 +end
spec/interactors/problem_destroy_spec.rb
@@ -7,7 +7,7 @@ describe ProblemDestroy do @@ -7,7 +7,7 @@ describe ProblemDestroy do
7 7
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])) 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])) 12 problem.stub(:comments).and_return(mock(:criteria, :only => [comment_1, comment_2]))
13 problem.stub(:delete) 13 problem.stub(:delete)
spec/models/app_spec.rb
@@ -23,6 +23,31 @@ describe App do @@ -23,6 +23,31 @@ describe App do
23 end 23 end
24 end 24 end
25 25
  26 + describe '<=>' do
  27 + it 'is compared by unresolved count' do
  28 + app_0 = stub_model(App, :name => 'app', :unresolved_count => 1, :problem_count => 1)
  29 + app_1 = stub_model(App, :name => 'app', :unresolved_count => 0, :problem_count => 1)
  30 +
  31 + app_0.should < app_1
  32 + app_1.should > app_0
  33 + end
  34 +
  35 + it 'is compared by problem count' do
  36 + app_0 = stub_model(App, :name => 'app', :unresolved_count => 0, :problem_count => 1)
  37 + app_1 = stub_model(App, :name => 'app', :unresolved_count => 0, :problem_count => 0)
  38 +
  39 + app_0.should < app_1
  40 + app_1.should > app_0
  41 + end
  42 +
  43 + it 'is compared by name' do
  44 + app_0 = stub_model(App, :name => 'app_0', :unresolved_count => 0, :problem_count => 0)
  45 + app_1 = stub_model(App, :name => 'app_1', :unresolved_count => 0, :problem_count => 0)
  46 +
  47 + app_0.should < app_1
  48 + app_1.should > app_0
  49 + end
  50 + end
26 51
27 context 'being created' do 52 context 'being created' do
28 it 'generates a new api-key' do 53 it 'generates a new api-key' do
spec/models/issue_trackers/bitbucket_issues_tracker_spec.rb
@@ -10,7 +10,7 @@ describe IssueTrackers::BitbucketIssuesTracker do @@ -10,7 +10,7 @@ describe IssueTrackers::BitbucketIssuesTracker do
10 10
11 number = 123 11 number = 123
12 @issue_link = "https://bitbucket.org/#{repo}/issue/#{number}/" 12 @issue_link = "https://bitbucket.org/#{repo}/issue/#{number}/"
13 - body = <<EOF 13 + body = <<EOF
14 { 14 {
15 "status": "new", 15 "status": "new",
16 "priority": "critical", 16 "priority": "critical",
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 = mock
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 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  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/notice_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe Notice do 3 describe Notice do
4 -  
5 -  
6 context 'validations' do 4 context 'validations' do
7 it 'requires a backtrace' do 5 it 'requires a backtrace' do
8 notice = Fabricate.build(:notice, :backtrace => nil) 6 notice = Fabricate.build(:notice, :backtrace => nil)
@@ -23,7 +21,6 @@ describe Notice do @@ -23,7 +21,6 @@ describe Notice do
23 end 21 end
24 end 22 end
25 23
26 -  
27 describe "key sanitization" do 24 describe "key sanitization" do
28 before do 25 before do
29 @hash = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}} 26 @hash = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}}
@@ -38,7 +35,6 @@ describe Notice do @@ -38,7 +35,6 @@ describe Notice do
38 end 35 end
39 end 36 end
40 37
41 -  
42 describe "user agent" do 38 describe "user agent" do
43 it "should be parsed and human-readable" do 39 it "should be parsed and human-readable" do
44 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' => {'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'}})
@@ -80,4 +76,11 @@ describe Notice do @@ -80,4 +76,11 @@ describe Notice do
80 notice.host.should == 'N/A' 76 notice.host.should == 'N/A'
81 end 77 end
82 end 78 end
  79 +
  80 + describe "request" do
  81 + it "returns empty hash if not set" do
  82 + notice = Notice.new
  83 + notice.request.should == {}
  84 + end
  85 + end
83 end 86 end
spec/models/notification_service/pushover_service_spec.rb
@@ -17,4 +17,4 @@ describe NotificationService::PushoverService do @@ -17,4 +17,4 @@ describe NotificationService::PushoverService do
17 17
18 notification_service.create_notification(problem) 18 notification_service.create_notification(problem)
19 end 19 end
20 -end  
21 \ No newline at end of file 20 \ No newline at end of file
  21 +end
spec/models/problem_spec.rb
@@ -24,7 +24,7 @@ describe Problem do @@ -24,7 +24,7 @@ describe Problem do
24 end 24 end
25 end 25 end
26 end 26 end
27 - 27 +
28 context '#last_notice_at' do 28 context '#last_notice_at' do
29 it "returns the created_at timestamp of the latest notice" do 29 it "returns the created_at timestamp of the latest notice" do
30 err = Fabricate(:err) 30 err = Fabricate(:err)
@@ -113,7 +113,7 @@ describe Problem do @@ -113,7 +113,7 @@ describe Problem do
113 problem = Fabricate(:problem, :notices_count => 1) 113 problem = Fabricate(:problem, :notices_count => 1)
114 original_notices_count = problem.notices_count 114 original_notices_count = problem.notices_count
115 original_notices_count.should > 0 115 original_notices_count.should > 0
116 - 116 +
117 problem.resolve! 117 problem.resolve!
118 problem.notices_count.should == original_notices_count 118 problem.notices_count.should == original_notices_count
119 end 119 end
spec/views/apps/index.html.haml_spec.rb
@@ -4,8 +4,6 @@ describe &quot;apps/index.html.haml&quot; do @@ -4,8 +4,6 @@ describe &quot;apps/index.html.haml&quot; 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 assign :apps, [app]
7 - assign :problem_counts, {app.id => 0}  
8 - assign :unresolved_counts, {app.id => 0}  
9 controller.stub(:current_user) { stub_model(User) } 7 controller.stub(:current_user) { stub_model(User) }
10 end 8 end
11 9