Commit a0af55302055ad839e6bd2cd838b0f5c16092780
Exists in
master
and in
1 other branch
Merge branch 'errbit' into feature/upstream_merge
Showing
135 changed files
with
571 additions
and
292 deletions
Show diff stats
Too many changes.
To preserve performance only 100 of 135 files displayed.
Gemfile
| @@ -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' |
Gemfile.lock
| @@ -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 |
README.md
| @@ -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
app/assets/images/alerts/important.gif
app/assets/images/alerts/info.gif
app/assets/images/alerts/title.gif
app/assets/images/bitbucket_create.png
app/assets/images/bitbucket_goto.png
app/assets/images/bitbucket_inactive.png
app/assets/images/campfire_create.png
app/assets/images/campfire_goto.png
app/assets/images/campfire_inactive.png
app/assets/images/fogbugz_create.png
app/assets/images/fogbugz_goto.png
app/assets/images/fogbugz_inactive.png
app/assets/images/github_create.png
app/assets/images/github_goto.png
app/assets/images/github_inactive.png
4.47 KB
4.47 KB
4.35 KB
app/assets/images/gtalk_create.png
app/assets/images/gtalk_goto.png
app/assets/images/gtalk_inactive.png
app/assets/images/hipchat_create.png
app/assets/images/hipchat_goto.png
app/assets/images/hipchat_inactive.png
app/assets/images/hoiio_create.png
app/assets/images/hoiio_goto.png
app/assets/images/hoiio_inactive.png
app/assets/images/lighthouseapp_create.png
app/assets/images/lighthouseapp_goto.png
app/assets/images/lighthouseapp_inactive.png
app/assets/images/loader.gif
app/assets/images/mingle_create.png
app/assets/images/mingle_goto.png
app/assets/images/mingle_inactive.png
app/assets/images/none_create.png
app/assets/images/none_inactive.png
app/assets/images/pivotal_create.png
app/assets/images/pivotal_goto.png
app/assets/images/pivotal_inactive.png
app/assets/images/pushover_create.png
app/assets/images/pushover_goto.png
app/assets/images/pushover_inactive.png
app/assets/images/redmine_create.png
app/assets/images/redmine_goto.png
app/assets/images/redmine_inactive.png
app/assets/images/thumbs-up.png
app/assets/javascripts/application.js.erb
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
app/assets/stylesheets/images/content-fade.png
app/assets/stylesheets/images/error-badge-bg.png
app/assets/stylesheets/images/header.png
app/assets/stylesheets/images/icons/add.png
app/assets/stylesheets/images/icons/briefcase.png
app/assets/stylesheets/images/icons/bullet-red-sm.png
app/assets/stylesheets/images/icons/cross.png
app/assets/stylesheets/images/icons/edit.png
app/assets/stylesheets/images/icons/error.png
app/assets/stylesheets/images/icons/github.png
app/assets/stylesheets/images/icons/ical.png
app/assets/stylesheets/images/icons/notice.png
app/assets/stylesheets/images/icons/required.png
app/assets/stylesheets/images/icons/right-arrow.png
app/assets/stylesheets/images/icons/success.png
app/assets/stylesheets/images/icons/thumbs-up.png
app/assets/stylesheets/images/icons/trash.png
app/assets/stylesheets/images/icons/unlink_github.png
app/assets/stylesheets/images/icons/up.png
app/assets/stylesheets/images/icons/user.png
app/assets/stylesheets/images/icons/warning.png
app/assets/stylesheets/images/logo.png
app/assets/stylesheets/images/notebook.png
app/assets/stylesheets/images/resolved-badge-bg.png
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 < InheritedResources::Base | @@ -52,23 +52,7 @@ class AppsController < 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 < InheritedResources::Base | @@ -80,7 +64,7 @@ class AppsController < 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 < ApplicationController | @@ -37,36 +35,10 @@ class ProblemsController < 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 < ApplicationController | @@ -87,13 +59,13 @@ class ProblemsController < 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 < ApplicationController | @@ -109,13 +81,13 @@ class ProblemsController < 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
| @@ -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 |
| @@ -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 < Mongoid::Observer | @@ -7,16 +7,7 @@ class NoticeObserver < 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 < NotificationService | @@ -21,6 +21,10 @@ class NotificationServices::HoiioService < 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 < NotificationService | @@ -17,6 +17,10 @@ class NotificationServices::PushoverService < 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 |