Commit 72e373804c57903d195f8b5e1957f0bc5a11f1b5

Authored by Brendon Rapp
2 parents 1eed79c8 fe1b7f5f
Exists in master and in 1 other branch production

Merge branch 'master' of github:errbit/errbit

Showing 66 changed files with 1136 additions and 563 deletions   Show diff stats
.gitignore
... ... @@ -18,3 +18,4 @@ bin
18 18 bundle
19 19 coverage
20 20 *#
  21 +.ruby-version
... ...
CHANGELOG.md
... ... @@ -4,12 +4,17 @@
4 4  
5 5 - Update some gems ([@shingara][])
6 6 - [#492][] Improve some Pjax call ([@nfedyashev][])
7   -- [#428][] Add the support of Unfuddle Tracker ([@parallel588][])
  7 +- [#428][] Add the support of Unfuddle Issue Tracker ([@parallel588][])
8 8 - Avoid to delete his own user ([@shingara][])
9 9 - [#456][] Avoid to delete admin access of current user logged ([@shingara][])
10 10 - [#253][] Refactor the Fingerprint generation ([@boblail][])
11 11 - [#508][] Merge comments to when you merge problems ([@shingara][])
12 12 - Update the Devise Gem to the last one ([@shingara][])
  13 +- [#524][] Add current user information on the notifer.js ([@roryf][])
  14 +- [#523][] Update javascript-stacktrace ([@aliscott][])
  15 +- [#516][] Add Jira Issue tracker ([@xenji][])
  16 +- [#512][] Add capabilities to configure the use of sendmail to send
  17 + email from Errbit ([@shingara][])
13 18  
14 19 ### Bug Fixes
15 20  
... ... @@ -76,24 +81,28 @@
76 81 [#506]: https://github.com/errbit/errbit/issues/506
77 82 [#508]: https://github.com/errbit/errbit/issues/508
78 83 [#514]: https://github.com/errbit/errbit/issues/514
  84 +[#516]: https://github.com/errbit/errbit/issues/516
79 85 [#517]: https://github.com/errbit/errbit/issues/517
  86 +[#524]: https://github.com/errbit/errbit/issues/524
80 87  
81 88 <!-- Contributor on Errbit Thanks to all of them -->
82 89  
  90 +[@Gonzih]: https://github.com/Gonzih
83 91 [@SamSaffron]: https://github.com/SamSaffron
84 92 [@adamjt]: https://github.com/adamjt
  93 +[@aliscott]: http://github.com/aliscott
85 94 [@alvarobp]: https://github.com/alvarobp
  95 +[@boblail]: https://github.com/boblail
86 96 [@chadcf]: https://github.com/chadcf
87   -[@mildavw]: https://github.com/mildavw
88   -[@sdepold]: https://github.com/sdepold
89   -[@shingara]: https://github.com/shingara
90   -[@tamaloa]: https://github.com/tamaloa
91 97 [@ivanyv]: https://github.com/ivanyv
92 98 [@manuelmeurer]: https://github.com/manuelmeurer
93   -[@tvdeyen]: https://github.com/tvdeyen
  99 +[@mildavw]: https://github.com/mildavw
  100 +[@mildavw]: https://github.com/mildavw
94 101 [@nfedyashev]: https://github.com/nfedyashev
95 102 [@parallel588]: https://github.com/parallel588
96   -[@Gonzih]: https://github.com/Gonzih
97   -[@boblail]: https://github.com/boblail
98 103 [@roryf]: https://github.com/roryf
99   -[@mildavw]: https://github.com/mildavw
  104 +[@sdepold]: https://github.com/sdepold
  105 +[@shingara]: https://github.com/shingara
  106 +[@tamaloa]: https://github.com/tamaloa
  107 +[@tvdeyen]: https://github.com/tvdeyen
  108 +[@xenji]: https://github.com/xenji
... ...
Gemfile
... ... @@ -11,7 +11,6 @@ gem &#39;htmlentities&#39;
11 11 gem 'rack-ssl', :require => 'rack/ssl' # force SSL
12 12  
13 13 gem 'useragent'
14   -gem 'inherited_resources'
15 14 gem 'decent_exposure'
16 15 gem 'strong_parameters'
17 16 gem 'SystemTimer', :platform => :ruby_18
... ... @@ -45,12 +44,15 @@ gem &#39;octokit&#39;
45 44 gem 'gitlab', :git => 'https://github.com/NARKOZ/gitlab.git'
46 45  
47 46 # Bitbucket Issues
48   -gem 'bitbucket_rest_api'
  47 +gem 'bitbucket_rest_api', :require => false
49 48  
50 49 # Unfuddle
51 50 gem "taskmapper", "~> 0.8.0"
52 51 gem "taskmapper-unfuddle", "~> 0.7.0"
53 52  
  53 +# Jira
  54 +gem 'jira-ruby', :require => 'jira'
  55 +
54 56 # Notification services
55 57 # ---------------------------------------
56 58 # Campfire ( We can't upgrade to 1.0 because drop support of ruby 1.8
... ... @@ -87,11 +89,9 @@ group :development, :test do
87 89 gem 'rspec-rails', '~> 2.6'
88 90 gem 'webmock', :require => false
89 91 gem 'airbrake', :require => false
90   - unless ENV["CI"]
91   - gem 'ruby-debug', :platform => :mri_18
92   - gem 'debugger', :platform => :mri_19
93   - gem 'pry-rails'
94   - end
  92 + gem 'ruby-debug', :platform => :mri_18
  93 + gem 'debugger', :platform => :mri_19
  94 + gem 'pry-rails'
95 95 # gem 'rpm_contrib'
96 96 # gem 'newrelic_rpm'
97 97 gem 'quiet_assets'
... ... @@ -118,7 +118,7 @@ group :test do
118 118 # DatabaseCleaner 1.0.0 drop the support of ruby 1.8.7
119 119 gem 'database_cleaner', '~> 0.9.0'
120 120 gem 'email_spec'
121   - gem 'timecop'
  121 + gem 'timecop', '0.6.1' # last version compatible to ruby 1.8
122 122 gem 'coveralls', :require => false
123 123 end
124 124  
... ...
Gemfile.lock
... ... @@ -40,13 +40,13 @@ GEM
40 40 activesupport (3.2.13)
41 41 i18n (= 0.6.1)
42 42 multi_json (~> 1.0)
43   - addressable (2.3.4)
  43 + addressable (2.3.5)
44 44 airbrake (3.1.12)
45 45 activesupport
46 46 builder
47 47 json
48 48 arel (3.0.2)
49   - bcrypt-ruby (3.0.1)
  49 + bcrypt-ruby (3.1.1)
50 50 better_errors (0.9.0)
51 51 coderay (>= 1.0.0)
52 52 erubis (>= 2.6.6)
... ... @@ -66,7 +66,7 @@ GEM
66 66 callsite (0.0.11)
67 67 campy (0.1.3)
68 68 multi_json (~> 1.0)
69   - capistrano (2.15.4)
  69 + capistrano (2.15.5)
70 70 highline
71 71 net-scp (>= 1.0.0)
72 72 net-sftp (>= 2.0.0)
... ... @@ -90,19 +90,19 @@ GEM
90 90 rest-client
91 91 simplecov (>= 0.7)
92 92 thor
93   - crack (0.4.0)
  93 + crack (0.4.1)
94 94 safe_yaml (~> 0.9.0)
95 95 css_parser (1.3.4)
96 96 addressable
97 97 daemons (1.1.9)
98 98 database_cleaner (0.9.1)
99 99 debug_inspector (0.0.2)
100   - debugger (1.6.0)
  100 + debugger (1.6.1)
101 101 columnize (>= 0.3.1)
102 102 debugger-linecache (~> 1.2.0)
103   - debugger-ruby_core_source (~> 1.2.1)
  103 + debugger-ruby_core_source (~> 1.2.3)
104 104 debugger-linecache (1.2.0)
105   - debugger-ruby_core_source (1.2.2)
  105 + debugger-ruby_core_source (1.2.3)
106 106 decent_exposure (2.2.0)
107 107 devise (2.2.4)
108 108 bcrypt-ruby (~> 3.0)
... ... @@ -111,7 +111,7 @@ GEM
111 111 warden (~> 1.2.1)
112 112 diff-lcs (1.2.4)
113 113 dotenv (0.8.0)
114   - email_spec (1.4.0)
  114 + email_spec (1.5.0)
115 115 launchy (~> 2.1)
116 116 mail (~> 2.2)
117 117 erubis (2.7.0)
... ... @@ -119,10 +119,11 @@ GEM
119 119 execjs (1.4.0)
120 120 multi_json (~> 1.0)
121 121 fabrication (1.3.2)
122   - faraday (0.8.7)
123   - multipart-post (~> 1.1)
  122 + faraday (0.8.8)
  123 + multipart-post (~> 1.2.0)
124 124 faraday_middleware (0.8.8)
125 125 faraday (>= 0.7.4, < 0.9)
  126 + ffi (1.9.0)
126 127 flowdock (0.3.1)
127 128 httparty (~> 0.7)
128 129 multi_json
... ... @@ -133,7 +134,6 @@ GEM
133 134 tilt
134 135 happymapper (0.4.0)
135 136 libxml-ruby (~> 2.0)
136   - has_scope (0.5.1)
137 137 hashie (1.2.0)
138 138 highline (1.6.19)
139 139 hike (1.2.3)
... ... @@ -151,9 +151,10 @@ GEM
151 151 multi_xml (>= 0.5.2)
152 152 httpauth (0.2.0)
153 153 i18n (0.6.1)
154   - inherited_resources (1.4.0)
155   - has_scope (~> 0.5.0)
156   - responders (~> 0.9)
  154 + jira-ruby (0.1.2)
  155 + activesupport
  156 + oauth
  157 + railties
157 158 journey (1.0.4)
158 159 jquery-rails (2.1.4)
159 160 railties (>= 3.0, < 5.0)
... ... @@ -181,7 +182,7 @@ GEM
181 182 callsite
182 183 rack-contrib
183 184 railties
184   - method_source (0.8.1)
  185 + method_source (0.8.2)
185 186 mime-types (1.23)
186 187 mongo (1.8.6)
187 188 bson (~> 1.8.6)
... ... @@ -197,16 +198,17 @@ GEM
197 198 multi_json (1.7.7)
198 199 multi_xml (0.5.4)
199 200 multipart-post (1.2.0)
200   - net-scp (1.1.1)
  201 + net-scp (1.1.2)
201 202 net-ssh (>= 2.6.5)
202 203 net-sftp (2.1.2)
203 204 net-ssh (>= 2.6.5)
204   - net-ssh (2.6.7)
  205 + net-ssh (2.6.8)
205 206 net-ssh-gateway (1.2.0)
206 207 net-ssh (>= 2.6.5)
207 208 nokogiri (1.5.10)
208 209 nokogiri-happymapper (0.5.7)
209 210 nokogiri (~> 1.5)
  211 + oauth (0.4.7)
210 212 oauth2 (0.8.1)
211 213 faraday (~> 0.8)
212 214 httpauth (~> 0.1)
... ... @@ -222,7 +224,7 @@ GEM
222 224 omniauth (1.1.4)
223 225 hashie (>= 1.2, < 3)
224 226 rack
225   - omniauth-github (1.1.0)
  227 + omniauth-github (1.1.1)
226 228 omniauth (~> 1.0)
227 229 omniauth-oauth2 (~> 1.1)
228 230 omniauth-oauth2 (1.1.1)
... ... @@ -251,7 +253,7 @@ GEM
251 253 coderay (~> 1.0.5)
252 254 method_source (~> 0.8)
253 255 slop (~> 3.4)
254   - pry-rails (0.3.1)
  256 + pry-rails (0.3.2)
255 257 pry (>= 0.9.10)
256 258 quiet_assets (1.0.2)
257 259 railties (>= 3.1, < 5.0)
... ... @@ -288,22 +290,20 @@ GEM
288 290 rdoc (3.12.2)
289 291 json (~> 1.4)
290 292 ref (1.0.5)
291   - responders (0.9.3)
292   - railties (~> 3.1)
293 293 rest-client (1.6.7)
294 294 mime-types (>= 1.16)
295 295 ri_cal (0.8.8)
296   - rspec-core (2.13.1)
297   - rspec-expectations (2.13.0)
  296 + rspec-core (2.14.4)
  297 + rspec-expectations (2.14.0)
298 298 diff-lcs (>= 1.1.3, < 2.0)
299   - rspec-mocks (2.13.1)
300   - rspec-rails (2.13.2)
  299 + rspec-mocks (2.14.2)
  300 + rspec-rails (2.14.0)
301 301 actionpack (>= 3.0)
302 302 activesupport (>= 3.0)
303 303 railties (>= 3.0)
304   - rspec-core (~> 2.13.0)
305   - rspec-expectations (~> 2.13.0)
306   - rspec-mocks (~> 2.13.0)
  304 + rspec-core (~> 2.14.0)
  305 + rspec-expectations (~> 2.14.0)
  306 + rspec-mocks (~> 2.14.0)
307 307 ruby-debug (0.10.4)
308 308 columnize (>= 0.1)
309 309 ruby-debug-base (~> 0.10.4.0)
... ... @@ -315,7 +315,7 @@ GEM
315 315 rushover (0.3.0)
316 316 json
317 317 rest-client
318   - safe_yaml (0.9.3)
  318 + safe_yaml (0.9.5)
319 319 selenium-webdriver (2.33.0)
320 320 childprocess (>= 0.2.5)
321 321 multi_json (~> 1.0)
... ... @@ -326,7 +326,7 @@ GEM
326 326 multi_json (~> 1.0)
327 327 simplecov-html (~> 0.7.1)
328 328 simplecov-html (0.7.1)
329   - slop (3.4.5)
  329 + slop (3.4.6)
330 330 sprockets (2.2.2)
331 331 hike (~> 1.2)
332 332 multi_json (~> 1.0)
... ... @@ -360,18 +360,18 @@ GEM
360 360 railties (> 3.2.8, < 4.0.0)
361 361 sprockets (>= 2.0.0)
362 362 tzinfo (0.3.37)
363   - uglifier (2.1.1)
  363 + uglifier (2.1.2)
364 364 execjs (>= 0.3.0)
365 365 multi_json (~> 1.0, >= 1.0.2)
366   - underscore-rails (1.4.4)
  366 + underscore-rails (1.5.1)
367 367 unicorn (4.6.3)
368 368 kgio (~> 2.6)
369 369 rack
370 370 raindrops (~> 0.7)
371 371 useragent (0.6.0)
372   - warden (1.2.1)
  372 + warden (1.2.3)
373 373 rack (>= 1.0)
374   - webmock (1.12.0)
  374 + webmock (1.13.0)
375 375 addressable (>= 2.2.7)
376 376 crack (>= 0.3.2)
377 377 websocket (1.0.7)
... ... @@ -412,7 +412,7 @@ DEPENDENCIES
412 412 hoptoad_notifier (~> 2.4)
413 413 htmlentities
414 414 httparty
415   - inherited_resources
  415 + jira-ruby
416 416 jquery-rails (~> 2.1.4)
417 417 kaminari (>= 0.14.1)
418 418 launchy
... ... @@ -442,7 +442,7 @@ DEPENDENCIES
442 442 taskmapper-unfuddle (~> 0.7.0)
443 443 therubyracer
444 444 thin
445   - timecop
  445 + timecop (= 0.6.1)
446 446 turbo-sprockets-rails3
447 447 uglifier (>= 1.0.3)
448 448 underscore-rails
... ...
README.md
... ... @@ -423,6 +423,18 @@ card_type = Defect, status = Open, priority = Essential
423 423 * Project id the id of your project where your ticket is create
424 424 * Milestone id the id of your milestone where your ticket is create
425 425  
  426 +** Jira Issue Integration **
  427 +
  428 +* base_url the jira URL
  429 +* context_path Context Path (Just "/" if empty otherwise with leading slash)
  430 +* username HTTP Basic Auth User
  431 +* password HTTP Basic Auth Password
  432 +* project_id The project Key where the issue will be created
  433 +* account Assign to this user. If empty, Jira takes the project default.
  434 +* issue_component Website - Other
  435 +* issue_type Issue type
  436 +* issue_priority Priority
  437 +
426 438  
427 439 What if Errbit has an error?
428 440 ----------------------------
... ...
app/assets/images/jira_active.png 0 → 100644

1.97 KB

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

1.97 KB

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

1.97 KB

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

1.91 KB

app/controllers/apps_controller.rb
1   -class AppsController < InheritedResources::Base
  1 +class AppsController < ApplicationController
  2 +
  3 + include ProblemsSearcher
  4 +
2 5 before_filter :require_admin!, :except => [:index, :show]
3 6 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update]
4 7 before_filter :parse_notice_at_notices_or_set_default, :only => [:create, :update]
5 8 respond_to :html
6 9  
7   - def show
8   - respond_to do |format|
9   - format.html do
10   - @all_errs = !!params[:all_errs]
  10 + expose(:app_scope) {
  11 + (current_user.admin? ? App : current_user.apps)
  12 + }
  13 +
  14 + expose(:apps) {
  15 + app_scope.all.sort
  16 + }
  17 +
  18 + expose(:app, :ancestor => :app_scope)
  19 +
  20 + expose(:all_errs) {
  21 + !!params[:all_errs]
  22 + }
  23 + expose(:problems) {
  24 + if request.format == :atom
  25 + app.problems.unresolved.ordered
  26 + else
  27 + pr = app.problems
  28 + pr = pr.unresolved unless all_errs
  29 + pr.in_env(
  30 + params[:environment]
  31 + ).ordered_by(params_sort, params_order).page(params[:page]).per(current_user.per_page)
  32 + end
  33 + }
11 34  
12   - @sort = params[:sort]
13   - @order = params[:order]
14   - @sort = "last_notice_at" unless %w{message app last_deploy_at last_notice_at count}.member?(@sort)
15   - @order = "desc" unless %w{asc desc}.member?(@order)
  35 + expose(:deploys) {
  36 + app.deploys.order_by(:created_at.desc).limit(5)
  37 + }
16 38  
17   - @problems = resource.problems
18   - @problems = @problems.unresolved unless @all_errs
19   - @problems = @problems.in_env(params[:environment]).ordered_by(@sort, @order).page(params[:page]).per(current_user.per_page)
  39 + def index; end
  40 + def show
  41 + app
  42 + end
20 43  
21   - @selected_problems = params[:problems] || []
22   - @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
23   - end
24   - format.atom do
25   - @problems = resource.problems.unresolved.ordered
26   - end
27   - end
  44 + def new
  45 + plug_params(app)
28 46 end
29 47  
30 48 def create
31   - @app = App.new(params[:app])
32 49 initialize_subclassed_issue_tracker
33 50 initialize_subclassed_notification_service
34   - create!
  51 + if app.save
  52 + redirect_to app_url(app), :flash => { :success => I18n.t('controllers.apps.flash.create.success') }
  53 + else
  54 + flash[:error] = I18n.t('controllers.apps.flash.create.error')
  55 + render :new
  56 + end
35 57 end
36 58  
37 59 def update
38   - @app = resource
39 60 initialize_subclassed_issue_tracker
40 61 initialize_subclassed_notification_service
41   - update!
  62 + if app.save
  63 + redirect_to app_url(app), :flash => { :success => I18n.t('controllers.apps.flash.update.success') }
  64 + else
  65 + flash[:error] = I18n.t('controllers.apps.flash.update.error')
  66 + render :edit
  67 + end
42 68 end
43 69  
44   - def new
45   - plug_params(build_resource)
46   - new!
  70 + def edit
  71 + plug_params(app)
47 72 end
48 73  
49   - def edit
50   - plug_params(resource)
51   - edit!
  74 + def destroy
  75 + if app.destroy
  76 + redirect_to apps_url, :flash => { :success => I18n.t('controllers.apps.flash.destroy.success') }
  77 + else
  78 + flash[:error] = I18n.t('controllers.apps.flash.destroy.error')
  79 + render :show
  80 + end
52 81 end
53 82  
54 83 protected
55   - def collection
56   - @apps ||= end_of_association_chain.all.sort
57   - end
58 84  
59 85 def initialize_subclassed_issue_tracker
60 86 # set the app's issue tracker
61 87 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type]
62 88 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type)
63   - @app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
  89 + app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
64 90 end
65 91 end
66 92 end
... ... @@ -69,21 +95,11 @@ class AppsController &lt; InheritedResources::Base
69 95 # set the app's notification service
70 96 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
71 97 if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
72   - @app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
  98 + app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
73 99 end
74 100 end
75 101 end
76 102  
77   - def begin_of_association_chain
78   - # Filter the @apps collection to apps watched by the current user, unless user is an admin.
79   - # If user is an admin, then no filter is applied, and all apps are shown.
80   - current_user unless current_user.admin?
81   - end
82   -
83   - def interpolation_options
84   - {:app_name => resource.name}
85   - end
86   -
87 103 def plug_params app
88 104 app.watchers.build if app.watchers.none?
89 105 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured?
... ...
app/controllers/problems_controller.rb
  1 +##
  2 +# Manage problems
  3 +#
  4 +# List of actions available :
  5 +# MEMBER => :show, :edit, :update, :create, :destroy, :resolve, :unresolve, :create_issue, :unlink_issue
  6 +# COLLECTION => :index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several, :search
1 7 class ProblemsController < ApplicationController
2   - before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several, :search]
3   - before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several, :search]
4   - before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
5   - before_filter :set_sorting_params, :only => [:index, :all, :search]
6   - before_filter :set_tracker_params, :only => [:create_issue]
7   -
8   - def index
9   - app_scope = current_user.admin? ? App.all : current_user.apps
10   - @all_errs = params[:all_errs]
11   - @problems = Problem.for_apps(app_scope).in_env(params[:environment]).all_else_unresolved(@all_errs).ordered_by(@sort, @order)
12   - @selected_problems = params[:problems] || []
13   - respond_to do |format|
14   - format.html do
15   - @problems = @problems.page(params[:page]).per(current_user.per_page)
16   - end
17   - format.atom
  8 +
  9 +
  10 + include ProblemsSearcher
  11 +
  12 + before_filter :need_selected_problem, :only => [
  13 + :resolve_several, :unresolve_several, :unmerge_several
  14 + ]
  15 +
  16 + expose(:app) {
  17 + if current_user.admin?
  18 + App.find(params[:app_id])
  19 + else
  20 + current_user.apps.find(params[:app_id])
18 21 end
19   - end
  22 + }
  23 +
  24 + expose(:problem) {
  25 + app.problems.find(params[:id])
  26 + }
  27 +
  28 +
  29 + expose(:all_errs) {
  30 + params[:all_errs]
  31 + }
  32 +
  33 + expose(:app_scope) {
  34 + apps = current_user.admin? ? App.all : current_user.apps
  35 + params[:app_id] ? apps.where(:_id => params[:app_id]) : apps
  36 + }
  37 +
  38 + expose(:params_environement) {
  39 + params[:environment]
  40 + }
  41 +
  42 + expose(:problems) {
  43 + pro = Problem.for_apps(
  44 + app_scope
  45 + ).in_env(
  46 + params_environement
  47 + ).all_else_unresolved(all_errs).ordered_by(params_sort, params_order)
  48 +
  49 + if request.format == :html
  50 + pro.page(params[:page]).per(current_user.per_page)
  51 + else
  52 + pro
  53 + end
  54 + }
  55 +
  56 + def index; end
20 57  
21 58 def show
22   - @notices = @problem.notices.reverse_ordered.page(params[:notice]).per(1)
  59 + @notices = problem.notices.reverse_ordered.page(params[:notice]).per(1)
23 60 @notice = @notices.first
24 61 @comment = Comment.new
25 62 end
26 63  
27 64 def create_issue
28   - issue_creation = IssueCreation.new(@problem, current_user, params[:tracker])
  65 + IssueTracker.update_url_options(request)
  66 + issue_creation = IssueCreation.new(problem, current_user, params[:tracker])
29 67  
30 68 unless issue_creation.execute
31   - flash[:error] = issue_creation.errors[:base].first
  69 + flash[:error] = issue_creation.errors.full_messages.join(', ')
32 70 end
33 71  
34   - redirect_to app_problem_path(@app, @problem)
  72 + redirect_to app_problem_path(app, problem)
35 73 end
36 74  
37 75 def unlink_issue
38   - @problem.update_attribute :issue_link, nil
39   - redirect_to app_problem_path(@app, @problem)
  76 + problem.update_attribute :issue_link, nil
  77 + redirect_to app_problem_path(app, problem)
40 78 end
41 79  
42 80 def resolve
43   - @problem.resolve!
  81 + problem.resolve!
44 82 flash[:success] = 'Great news everyone! The err has been resolved.'
45 83 redirect_to :back
46 84 rescue ActionController::RedirectBackError
47   - redirect_to app_path(@app)
  85 + redirect_to app_path(app)
48 86 end
49 87  
50 88 def resolve_several
51   - @selected_problems.each(&:resolve!)
52   - flash[:success] = "Great news everyone! #{I18n.t(:n_errs_have, :count => @selected_problems.count)} been resolved."
  89 + selected_problems.each(&:resolve!)
  90 + flash[:success] = "Great news everyone! #{I18n.t(:n_errs_have, :count => selected_problems.count)} been resolved."
53 91 redirect_to :back
54 92 end
55 93  
56 94 def unresolve_several
57   - @selected_problems.each(&:unresolve!)
58   - flash[:success] = "#{I18n.t(:n_errs_have, :count => @selected_problems.count)} been unresolved."
  95 + selected_problems.each(&:unresolve!)
  96 + flash[:success] = "#{I18n.t(:n_errs_have, :count => selected_problems.count)} been unresolved."
59 97 redirect_to :back
60 98 end
61 99  
  100 + ##
  101 + # Action to merge several Problem in One problem
  102 + #
  103 + # @param [ Array<String> ] :problems the list of problem ids
  104 + #
62 105 def merge_several
63   - if @selected_problems.length < 2
64   - flash[:notice] = "You must select at least two errors to merge"
  106 + if selected_problems.length < 2
  107 + flash[:notice] = I18n.t('controllers.problems.flash.need_two_errors_merge')
65 108 else
66   - @merged_problem = Problem.merge!(@selected_problems)
67   - flash[:notice] = "#{@selected_problems.count} errors have been merged."
  109 + ProblemMerge.new(selected_problems).merge
  110 + flash[:notice] = I18n.t('controllers.problems.flash.merge_several.success', :nb => selected_problems.count)
68 111 end
69 112 redirect_to :back
70 113 end
71 114  
72 115 def unmerge_several
73   - all = @selected_problems.map(&:unmerge!).flatten
  116 + all = selected_problems.map(&:unmerge!).flatten
74 117 flash[:success] = "#{I18n.t(:n_errs_have, :count => all.length)} been unmerged."
75 118 redirect_to :back
76 119 end
77 120  
78 121 def destroy_several
79   - nb_problem_destroy = ProblemDestroy.execute(@selected_problems)
  122 + nb_problem_destroy = ProblemDestroy.execute(selected_problems)
80 123 flash[:notice] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted."
81 124 redirect_to :back
82 125 end
83 126  
84 127 def search
85   - if params[:app_id]
86   - app_scope = App.where(:_id => params[:app_id])
87   - else
88   - app_scope = current_user.admin? ? App.all : current_user.apps
89   - end
90   - @problems = Problem.search(params[:search]).for_apps(app_scope).in_env(params[:environment]).all_else_unresolved(params[:all_errs]).ordered_by(@sort, @order)
  128 + @problems = Problem.search(params[:search]).for_apps(app_scope).in_env(params[:environment]).all_else_unresolved(params[:all_errs]).ordered_by(params_sort, params_order)
91 129 @selected_problems = params[:problems] || []
92 130 @problems = @problems.page(params[:page]).per(current_user.per_page)
93 131 render :content_type => 'text/javascript'
94 132 end
95 133  
96 134 protected
97   - def find_app
98   - @app = App.find(params[:app_id])
99   -
100   - # Mongoid Bug: could not chain: current_user.apps.find_by_id!
101   - # apparently finding by 'watchers.email' and 'id' is broken
102   - raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
103   - end
104 135  
105   - def find_problem
106   - @problem = @app.problems.find(params[:id])
107   - end
108   -
109   - def set_tracker_params
110   - IssueTracker.default_url_options[:host] = request.host
111   - IssueTracker.default_url_options[:port] = request.port
112   - IssueTracker.default_url_options[:protocol] = request.scheme
113   - end
114   -
115   - def find_selected_problems
116   - err_ids = (params[:problems] || []).compact
117   - if err_ids.empty?
118   - flash[:notice] = "You have not selected any errors"
119   - redirect_to :back
120   - else
121   - @selected_problems = Array(Problem.find(err_ids))
122   - end
123   - end
124   -
125   - def set_sorting_params
126   - @sort = params[:sort]
127   - @sort = "last_notice_at" unless %w{app message last_notice_at last_deploy_at count}.member?(@sort)
128   - @order = params[:order] || "desc"
  136 + ##
  137 + # Redirect :back if no errors selected
  138 + #
  139 + def need_selected_problem
  140 + if err_ids.empty?
  141 + flash[:notice] = I18n.t('controllers.problems.flash.no_select_problem')
  142 + redirect_to :back
129 143 end
  144 + end
130 145 end
131 146  
... ...
app/controllers/problems_searcher.rb 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +# Include to do a Search
  2 +# TODO: Need to be in a Dedicated Object ProblemsSearch with params like input
  3 +#
  4 +module ProblemsSearcher
  5 + extend ActiveSupport::Concern
  6 +
  7 + included do
  8 +
  9 + expose(:params_sort) {
  10 + unless %w{app message last_notice_at last_deploy_at count}.member?(params[:sort])
  11 + "last_notice_at"
  12 + else
  13 + params[:sort]
  14 + end
  15 + }
  16 +
  17 + expose(:params_order){
  18 + unless %w{asc desc}.member?(params[:order])
  19 + 'desc'
  20 + else
  21 + params[:order]
  22 + end
  23 + }
  24 +
  25 + expose(:selected_problems) {
  26 + Array(Problem.find(err_ids))
  27 + }
  28 +
  29 + expose(:err_ids) {
  30 + (params[:problems] || []).compact
  31 + }
  32 +
  33 + end
  34 +end
... ...
app/helpers/apps_helper.rb
... ... @@ -41,7 +41,7 @@ module AppsHelper
41 41 def detect_any_apps_with_attributes
42 42 @any_github_repos = @any_issue_trackers = @any_deploys = @any_bitbucket_repos = @any_notification_services = false
43 43  
44   - @apps.each do |app|
  44 + apps.each do |app|
45 45 @any_github_repos ||= app.github_repo?
46 46 @any_bitbucket_repos ||= app.bitbucket_repo?
47 47 @any_issue_trackers ||= app.issue_tracker_configured?
... ...
app/helpers/sort_helper.rb
1 1 # encoding: utf-8
2 2 module SortHelper
3   -
  3 +
4 4 def link_for_sort(name, field=nil)
5 5 field ||= name.underscore
6   - current = (@sort == field)
7   - order = (current && (@order == "asc")) ? "desc" : "asc"
  6 + current = (params_sort == field)
  7 + order = (current && (params_order == "asc")) ? "desc" : "asc"
8 8 url = request.path + "?sort=#{field}&order=#{order}"
9 9 options = {}
10 10 options.merge!(:class => "current #{order}") if current
11 11 link_to(name, url, options)
12 12 end
13   -
  13 +
14 14 end
... ...
app/interactors/issue_creation.rb
... ... @@ -41,15 +41,11 @@ class IssueCreation
41 41 end
42 42  
43 43 def execute
44   - if tracker
45   - begin
46   - tracker.create_issue problem, user
47   - rescue => ex
48   - Rails.logger.error "Error during issue creation: " << ex.message
49   - errors.add :base, "There was an error during issue creation: #{ex.message}"
50   - end
51   - end
52   -
  44 + tracker.create_issue problem, user if tracker
53 45 errors.empty?
  46 + rescue => ex
  47 + Rails.logger.error "Error during issue creation: " << ex.message
  48 + errors.add :base, "There was an error during issue creation: #{ex.message}"
  49 + false
54 50 end
55 51 end
... ...
app/models/issue_tracker.rb
... ... @@ -17,6 +17,14 @@ class IssueTracker
17 17 field :subdomain, :type => String
18 18 field :milestone_id, :type => String
19 19  
  20 + # Is there any better way to enhance the props? Putting them into the subclass leads to
  21 + # an error while rendering the form fields -.-
  22 + field :base_url, :type => String
  23 + field :context_path, :type => String
  24 + field :issue_type, :type => String
  25 + field :issue_component, :type => String
  26 + field :issue_priority, :type => String
  27 +
20 28 validate :check_params
21 29  
22 30 # Subclasses are responsible for overwriting this method.
... ... @@ -40,4 +48,15 @@ class IssueTracker
40 48 def configured?
41 49 project_id.present?
42 50 end
  51 +
  52 + ##
  53 + # Update default_url_option with valid data from the request information
  54 + #
  55 + # @param [ Request ] a request with host, port and protocol
  56 + #
  57 + def self.update_url_options(request)
  58 + IssueTracker.default_url_options[:host] = request.host
  59 + IssueTracker.default_url_options[:port] = request.port
  60 + IssueTracker.default_url_options[:protocol] = request.scheme
  61 + end
43 62 end
... ...
app/models/issue_trackers/bitbucket_issues_tracker.rb
  1 +require 'bitbucket_rest_api'
1 2 class IssueTrackers::BitbucketIssuesTracker < IssueTracker
2 3 Label = "bitbucket"
3 4 Note = 'Please configure your Bitbucket repository in the <strong>BITBUCKET REPO</strong> field above.'
... ...
app/models/issue_trackers/jira_tracker.rb 0 → 100644
... ... @@ -0,0 +1,109 @@
  1 +if defined? JIRA
  2 + class IssueTrackers::JiraTracker < IssueTracker
  3 + Label = 'jira'
  4 +
  5 + Fields = [
  6 + [:base_url, {
  7 + :label => 'Jira URL without trailing slash',
  8 + :placeholder => 'https://jira.example.org/'
  9 + }],
  10 + [:context_path, {
  11 + :optional => true,
  12 + :label => 'Context Path (Just "/" if empty otherwise with leading slash)',
  13 + :placeholder => "/jira"
  14 + }],
  15 + [:username, {
  16 + :optional => true,
  17 + :label => 'HTTP Basic Auth User',
  18 + :placeholder => 'johndoe'
  19 + }],
  20 + [:password, {
  21 + :optional => true,
  22 + :label => 'HTTP Basic Auth Password',
  23 + :placeholder => 'p@assW0rd'
  24 + }],
  25 + [:project_id, {
  26 + :label => 'Project Key',
  27 + :placeholder => 'The project Key where the issue will be created'
  28 + }],
  29 + [:account, {
  30 + :optional => true,
  31 + :label => 'Assign to this user. If empty, Jira takes the project default.',
  32 + :placeholder => "username"
  33 + }],
  34 + [:issue_component, {
  35 + :label => 'Issue category',
  36 + :placeholder => 'Website - Other'
  37 + }],
  38 + [:issue_type, {
  39 + :label => 'Issue type',
  40 + :placeholder => 'Bug'
  41 + }],
  42 + [:issue_priority, {
  43 + :label => 'Priority',
  44 + :placeholder => 'Normal'
  45 + }]
  46 + ]
  47 +
  48 + def check_params
  49 + if Fields.detect { |f| self[f[0]].blank? && !f[1][:optional] }
  50 + errors.add :base, 'You must specify all non optional values!'
  51 + end
  52 + end
  53 +
  54 +
  55 + #
  56 + # @param problem Problem
  57 + def create_issue(problem, reported_by = nil)
  58 + options = {
  59 + :username => username,
  60 + :password => password,
  61 + :site => base_url,
  62 + :context_path => context_path,
  63 + :auth_type => :basic
  64 + }
  65 + client = JIRA::Client.new(options)
  66 +
  67 + issue = {
  68 + :fields => {
  69 + :project => {
  70 + :key => project_id
  71 + },
  72 + :summary => issue_title(problem),
  73 + :description => body_template.result(binding),
  74 + :issuetype => {
  75 + :name => issue_type
  76 + },
  77 + :priority => {
  78 + :name => issue_priority,
  79 + },
  80 +
  81 + :components => [{:name => issue_component}]
  82 + }
  83 + }
  84 +
  85 + issue[:fields][:assignee] = {:name => account} if account
  86 +
  87 + issue_build = client.Issue.build
  88 + issue_build.save(issue)
  89 + issue_build.fetch
  90 +
  91 + problem.update_attributes(
  92 + :issue_link => "#{base_url}#{context_path}browse/#{issue_build.key}",
  93 + :issue_type => Label
  94 + )
  95 +
  96 + # Maybe in a later version?
  97 + #remote_link = {
  98 + # :url => app_problem_url(problem.app, problem),
  99 + # :name => "Link to Errbit Issue"
  100 + #}
  101 + #remote_link_build = issue_build.remotelink.build
  102 + #remote_link_build.save(remote_link)
  103 + end
  104 +
  105 + def body_template
  106 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/jira_body.txt.erb"))
  107 + end
  108 + end
  109 +end
0 110 \ No newline at end of file
... ...
app/views/apps/_fields.html.haml
1   -= errors_for @app
  1 += errors_for app
2 2  
3 3 %div.required
4 4 = f.label :name
... ...
app/views/apps/edit.html.haml
1 1 - content_for :title, 'Edit App'
2 2 - content_for :action_bar do
3 3 = link_to_copy_attributes_from_other_app
4   - = link_to 'destroy application', app_path(@app), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button'
5   - = link_to('cancel', app_path(@app), :class => 'button')
  4 + = link_to 'destroy application', app_path(app), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button'
  5 + = link_to('cancel', app_path(app), :class => 'button')
6 6  
7   -= form_for @app do |f|
  7 += form_for app do |f|
8 8  
9 9 = render 'fields', :f => f
10 10  
... ...
app/views/apps/index.html.haml
... ... @@ -16,7 +16,7 @@
16 16 %th Last Deploy
17 17 %th Errors
18 18 %tbody
19   - - @apps.each do |app|
  19 + - apps.each do |app|
20 20 %tr
21 21 %td.name= link_to app.name, app_path(app)
22 22 - if any_github_repos? or any_bitbucket_repos?
... ... @@ -50,7 +50,7 @@
50 50 - if app.problem_count > 0
51 51 - unresolved = app.unresolved_count
52 52 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
53   - - if @apps.none?
  53 + - if apps.none?
54 54 %tr
55 55 %td{:colspan => 3}
56 56 %em
... ...
app/views/apps/new.html.haml
... ... @@ -3,7 +3,7 @@
3 3 = link_to_copy_attributes_from_other_app
4 4 = link_to('cancel', apps_path, :class => 'button')
5 5  
6   -= form_for @app do |f|
  6 += form_for app do |f|
7 7  
8 8 = render 'fields', :f => f
9 9  
... ...
app/views/apps/show.atom.builder
1 1 atom_feed do |feed|
2   - feed.title("Errbit notices for #{h @app.name} at #{root_url}")
  2 + feed.title("Errbit notices for #{h app.name} at #{root_url}")
3 3 render "problems/list", :feed => feed
4 4 end
... ...
app/views/apps/show.html.haml
1   -- content_for :title, @app.name
  1 +- content_for :title, app.name
2 2 - content_for :head do
3   - = auto_discovery_link_tag :atom, app_path(@app, User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices for #{@app.name} at #{request.host}"
  3 + = auto_discovery_link_tag :atom, app_path(app, User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices for #{app.name} at #{request.host}"
4 4 - content_for :meta do
5 5 %strong Errors Caught:
6   - = @app.problems.count
  6 + = app.problems.count
7 7 %strong Deploy Count:
8   - = @app.deploys.count
  8 + = app.deploys.count
9 9 %strong API Key:
10   - = @app.api_key
  10 + = app.api_key
11 11 - content_for :action_bar do
12 12 - if current_user.admin?
13   - = link_to 'edit', edit_app_path(@app), :class => 'button'
14   - - if @all_errs
15   - = link_to 'unresolved errs', app_path(@app), :class => 'button'
  13 + = link_to 'edit', edit_app_path(app), :class => 'button'
  14 + - if all_errs
  15 + = link_to 'unresolved errs', app_path(app), :class => 'button'
16 16 - else
17   - = link_to 'all errs', app_path(@app, :all_errs => true), :class => 'button'
  17 + = link_to 'all errs', app_path(app, :all_errs => true), :class => 'button'
18 18  
19 19 %h3#watchers_toggle
20 20 Watchers
21 21 %span.click_span (show/hide)
22 22 #watchers_div
23   - - if @app.notify_all_users
  23 + - if app.notify_all_users
24 24 %table.watchers
25 25 %thead
26 26 %tr
... ... @@ -31,15 +31,15 @@
31 31 %tr
32 32 %th User or Email
33 33 %tbody
34   - - @app.watchers.each do |watcher|
  34 + - app.watchers.each do |watcher|
35 35 %tr
36 36 %td= watcher.label
37   - - if @app.watchers.none?
  37 + - if app.watchers.none?
38 38 %tr
39 39 %td
40 40 %em Sadly, no one is watching this app
41 41  
42   -- if @app.github_repo?
  42 +- if app.github_repo?
43 43 %h3#repository_toggle
44 44 Repository
45 45 %span.click_span (show/hide)
... ... @@ -50,13 +50,13 @@
50 50 %th GitHub Repo
51 51 %tbody
52 52 %tr
53   - %td= link_to(@app.github_repo, @app.github_url, :target => '_blank')
  53 + %td= link_to(app.github_repo, app.github_url, :target => '_blank')
54 54  
55 55 %h3#deploys_toggle
56 56 Latest Deploys
57 57 %span.click_span (show/hide)
58 58 #deploys_div
59   - - if @deploys.any?
  59 + - if deploys.any?
60 60 %table.deploys
61 61 %thead
62 62 %tr
... ... @@ -68,7 +68,7 @@
68 68 %th Revision
69 69  
70 70 %tbody
71   - - @deploys.each do |deploy|
  71 + - deploys.each do |deploy|
72 72 %tr
73 73 %td.when #{deploy.created_at.to_s(:micro)}
74 74 %td.environment #{deploy.environment}
... ... @@ -76,20 +76,20 @@
76 76 %td.message #{deploy.message}
77 77 %td.repository #{deploy.repository}
78 78 %td.revision #{deploy.short_revision}
79   - = link_to "All Deploys (#{@app.deploys.count})", app_deploys_path(@app), :class => 'button'
  79 + = link_to "All Deploys (#{app.deploys.count})", app_deploys_path(app), :class => 'button'
80 80 - else
81 81 %h3 No deploys
82 82  
83   -- if @app.problems.any?
  83 +- if app.problems.any?
84 84 %h3.clear Errors
85 85 %section
86   - = form_tag search_problems_path(:all_errs => @all_errs, :app_id => @app.id), :method => :get, :remote => true do
  86 + = form_tag search_problems_path(:all_errs => all_errs, :app_id => app.id), :method => :get, :remote => true do
87 87 = text_field_tag :search, params[:search], :placeholder => 'Search for issues'
88 88 %br
89 89 %section
90 90 .problem_table{:id => 'problem_table'}
91   - = render 'problems/table', :problems => @problems
  91 + = render 'problems/table', :problems => problems
92 92 - else
93 93 %h3.clear No errs have been caught yet, make sure you setup your app
94   - = render 'configuration_instructions', :app => @app
  94 + = render 'configuration_instructions', :app => app
95 95  
... ...
app/views/issue_trackers/jira_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +<% if notice = problem.notices.first %>
  2 +h2. Summary
  3 +<% if notice.request['url'].present? %>
  4 +h3. URL
  5 +
  6 +"<%= notice.request['url'] %>":<%= notice.request['url'] %>
  7 +<% end %>
  8 +h3. Where
  9 +
  10 +<%= notice.where %>
  11 +
  12 +h3. When
  13 +
  14 +<%= notice.created_at.to_s(:micro) %>
  15 +
  16 +"More Details on Errbit":<%= app_problem_url problem.app, problem %>
  17 +<% end %>
0 18 \ No newline at end of file
... ...
app/views/problems/_issue_tracker_links.html.haml
1   -- if @app.issue_tracker_configured? || current_user.github_account?
2   - - if @problem.issue_link.present?
3   - %span= link_to 'go to issue', @problem.issue_link, :class => "#{@problem.issue_type}_goto goto-issue"
4   - = link_to 'unlink issue', unlink_issue_app_problem_path(@app, @problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue"
5   - - elsif @problem.issue_link == "pending"
6   - %span.disabled= link_to 'creating...', '#', :class => "#{@problem.issue_type}_inactive create-issue"
7   - = link_to 'retry', create_issue_app_problem_path(@app, @problem), :method => :post
  1 +- if app.issue_tracker_configured? || current_user.github_account?
  2 + - if problem.issue_link.present?
  3 + %span= link_to 'go to issue', problem.issue_link, :class => "#{problem.issue_type}_goto goto-issue"
  4 + = link_to 'unlink issue', unlink_issue_app_problem_path(app, problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue"
  5 + - elsif problem.issue_link == "pending"
  6 + %span.disabled= link_to 'creating...', '#', :class => "#{problem.issue_type}_inactive create-issue"
  7 + = link_to 'retry', create_issue_app_problem_path(app, problem), :method => :post
8 8 - else
9   - - if @app.github_repo?
  9 + - if app.github_repo?
10 10 - if current_user.can_create_github_issues?
11   - %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem, :tracker => 'user_github'), :method => :post, :class => "github_create create-issue"
12   - - elsif @app.issue_tracker_configured? && @app.issue_tracker.label.eql?('github')
13   - %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem), :method => :post, :class => "github_create create-issue"
14   - - if @app.issue_tracker_configured? && !@app.issue_tracker.label.eql?('github')
15   - %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem), :method => :post, :class => "#{@app.issue_tracker.label}_create create-issue"
  11 + %span= link_to 'create issue', create_issue_app_problem_path(app, problem, :tracker => 'user_github'), :method => :post, :class => "github_create create-issue"
  12 + - elsif app.issue_tracker_configured? && app.issue_tracker.label.eql?('github')
  13 + %span= link_to 'create issue', create_issue_app_problem_path(app, problem), :method => :post, :class => "github_create create-issue"
  14 + - if app.issue_tracker_configured? && !app.issue_tracker.label.eql?('github')
  15 + %span= link_to 'create issue', create_issue_app_problem_path(app, problem), :method => :post, :class => "#{app.issue_tracker.label}_create create-issue"
... ...
app/views/problems/_list.atom.builder
1   -feed.updated(@problems.first.try(:created_at) || Time.now)
  1 +feed.updated(problems.first.try(:created_at) || Time.now)
2 2  
3   -for problem in @problems
  3 +for problem in problems
4 4 notice = problem.notices.first
5 5  
6   - feed.entry(problem, :url => app_problem_url(problem.app, problem)) do |entry|
  6 + feed.entry(problem, :url => app_problem_url(problem.app.to_param, problem.to_param)) do |entry|
7 7 entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}"
8 8 entry.author do |author|
9 9 author.name "#{ problem.app.name } [#{ problem.environment }]"
... ...
app/views/problems/_table.html.haml
... ... @@ -16,7 +16,7 @@
16 16 - problems.each do |problem|
17 17 %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'}
18 18 %td.select
19   - = check_box_tag "problems[]", problem.id, @selected_problems.member?(problem.id.to_s)
  19 + = check_box_tag "problems[]", problem.id, selected_problems.member?(problem.id.to_s)
20 20 %td.app
21 21 = link_to problem.app.name, app_path(problem.app)
22 22 - if current_page?(:controller => 'problems')
... ...
app/views/problems/index.html.haml
1 1 - content_for :title, @all_errs ? 'All Errors' : 'Unresolved Errors'
2 2 - content_for :head do
3 3 = auto_discovery_link_tag :atom, problems_path(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{request.host}"
  4 +
4 5 - content_for :action_bar do
5 6 - if @all_errs
6 7 = link_to 'hide resolved', problems_path, :class => 'button'
7 8 - else
8 9 = link_to 'show resolved', problems_path(:all_errs => true), :class => 'button'
  10 +
9 11 %section
10 12 = form_tag search_problems_path(:all_errs => @all_errs), :method => :get, :remote => true do
11 13 = text_field_tag :search, params[:search], :placeholder => 'Search for issues'
12 14 %br
13 15 %section
14   - .problem_table{:id => 'problem_table'}
15   - = render 'problems/table', :problems => @problems
  16 + #problem_table.problem_table
  17 + = render 'problems/table'
... ...
app/views/problems/show.html.haml
1   -- content_for :page_title, @problem.message
  1 +- content_for :page_title, problem.message
2 2 - content_for :title_css_class, 'err_show'
3   -- content_for :title, @problem.error_class || truncate(@problem.message, :length => 32)
  3 +- content_for :title, problem.error_class || truncate(problem.message, :length => 32)
4 4 - content_for :meta do
5 5 %strong App:
6   - = link_to @app.name, @app
  6 + = link_to app.name, app
7 7 %strong Where:
8   - = @problem.where
  8 + = problem.where
9 9 %br
10 10 %strong Environment:
11   - = @problem.environment
  11 + = problem.environment
12 12 %strong Last Notice:
13   - = @problem.last_notice_at.to_s(:precise)
  13 + = problem.last_notice_at.to_s(:precise)
14 14 - content_for :action_bar do
15   - - if @problem.unresolved?
16   - %span= link_to 'resolve', [:resolve, @app, @problem], :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve'
  15 + - if problem.unresolved?
  16 + %span= link_to 'resolve', [:resolve, app, problem], :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve'
17 17 - if current_user.authentication_token
18   - %span= link_to 'iCal', polymorphic_path([@app, @problem], :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link"
19   - %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_problems_path(@app)), :class => 'up'
  18 + %span= link_to 'iCal', polymorphic_path([app, problem], :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link"
  19 + %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_problems_path(app)), :class => 'up'
20 20 %br
21 21 = render "issue_tracker_links"
22 22  
23   -- if @problem.comments_allowed? || @problem.comments.any?
  23 +- if problem.comments_allowed? || problem.comments.any?
24 24 - content_for :comments do
25 25 %h3 Comments
26   - - @problem.comments.each do |comment|
  26 + - problem.comments.each do |comment|
27 27 .window
28 28 %table.comment
29 29 %tr
... ... @@ -37,11 +37,11 @@
37 37 - else
38 38 %span.comment-info
39 39 = time_ago_in_words(comment.created_at, true) << " ago by [Unknown User]"
40   - %span.delete= link_to '&#10008;'.html_safe, [@app, @problem, comment], :method => :delete, :data => { :confirm => "Are you sure you don't need this comment?" }, :class => "destroy-comment"
  40 + %span.delete= link_to '&#10008;'.html_safe, [app, problem, comment], :method => :delete, :data => { :confirm => "Are you sure you don't need this comment?" }, :class => "destroy-comment"
41 41 %tr
42 42 %td= simple_format comment.body
43   - - if @problem.comments_allowed?
44   - = form_for [@app, @problem, @comment] do |comment_form|
  43 + - if problem.comments_allowed?
  44 + = form_for [app, problem, @comment] do |comment_form|
45 45 %p Add a comment
46 46 = comment_form.text_area :body
47 47 = comment_form.submit "Save Comment"
... ... @@ -63,7 +63,7 @@
63 63 - if @notice
64 64 #summary
65 65 %h3 Summary
66   - = render 'notices/summary', :notice => @notice, :problem => @problem
  66 + = render 'notices/summary', :notice => @notice
67 67  
68 68 #backtrace
69 69 %h3 Backtrace
... ...
app/views/problems/show.ics.haml
1   -= generate_problem_ical(@problem.notices.order_by(:created_at.asc))
  1 += generate_problem_ical(problem.notices.order_by(:created_at.asc))
... ...
config/config.example.yml
... ... @@ -102,3 +102,8 @@ github_access_scope: [&#39;repo&#39;]
102 102 # :user_name: USERNAME
103 103 # :password: PASSWORD
104 104  
  105 +
  106 +# If you want send your email by your sendmail
  107 +# sendmail_settings:
  108 +# :location: '/usr/sbin/sendmail'
  109 +# :arguments: '-i -t'
... ...
config/initializers/_load_config.rb
... ... @@ -71,6 +71,11 @@ if smtp = Errbit::Config.smtp_settings
71 71 ActionMailer::Base.smtp_settings = smtp
72 72 end
73 73  
  74 +if sendmail = Errbit::Config.sendmail_settings
  75 + ActionMailer::Base.delivery_method = :sendmail
  76 + ActionMailer::Base.sendmail_settings = sendmail
  77 +end
  78 +
74 79 # Set config specific values
75 80 (ActionMailer::Base.default_url_options ||= {}).tap do |default|
76 81 default.merge! :host => Errbit::Config.host if default[:host].blank?
... ...
config/initializers/inherited_resources.rb
... ... @@ -1,2 +0,0 @@
1   -InheritedResources.flash_keys = [:success, :error]
2   -
config/locales/en.yml
... ... @@ -2,7 +2,6 @@
2 2 # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 3  
4 4 en:
5   - hello: "Hello world"
6 5 flash:
7 6 apps:
8 7 create:
... ... @@ -11,6 +10,7 @@ en:
11 10 success: "Good news everyone! '%{app_name}' was successfully updated."
12 11 destroy:
13 12 success: "'%{app_name}' was successfully destroyed."
  13 +
14 14 n_errs_have:
15 15 one: "%{count} err has"
16 16 other: "%{count} errs have"
... ... @@ -31,6 +31,18 @@ en:
31 31 edit_profile: 'Edit profile'
32 32  
33 33 controllers:
  34 + apps:
  35 + flash:
  36 + create:
  37 + success: "Your app was successfully created."
  38 + error: "You app was successfully destroyed."
  39 + update:
  40 + success: "You app was successfully updated."
  41 + error: "You app was not updated"
  42 + destroy:
  43 + success: "You app was successfully destroyed."
  44 + error: "You app could not be destroyed."
  45 +
34 46 users:
35 47 flash:
36 48 destroy:
... ... @@ -38,6 +50,13 @@ en:
38 50 error: "You can't delete yourself"
39 51 update:
40 52 success: "%{name}'s information was successfully updated."
  53 + problems:
  54 + flash:
  55 + no_select_problem: "You have not selected any errors"
  56 + need_two_errors_merge: "You must select at least two errors to merge"
  57 + merge_several:
  58 + success: "%{nb} errors have been merged."
  59 +
41 60  
42 61 devise:
43 62 registrations:
... ...
public/javascripts/notifier.js
... ... @@ -47,76 +47,112 @@
47 47 // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48 48  
49 49 /**
50   - * Main function giving a function stack trace with a forced or passed in Error
  50 + * Main function giving a function stack trace with a forced or passed in Error
51 51 *
52 52 * @cfg {Error} e The error to create a stacktrace from (optional)
53 53 * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
54   - * @return {Array} of Strings with functions, lines, files, and arguments where possible
  54 + * @return {Array} of Strings with functions, lines, files, and arguments where possible
55 55 */
56 56 function printStackTrace(options) {
57   - var ex = (options && options.e) ? options.e : null;
58   - var guess = options ? !!options.guess : true;
59   -
60   - var p = new printStackTrace.implementation();
61   - var result = p.run(ex);
62   - return (guess) ? p.guessFunctions(result) : result;
  57 + options = options || {guess: true};
  58 + var ex = options.e || null, guess = !!options.guess;
  59 + var p = new printStackTrace.implementation(), result = p.run(ex);
  60 + return (guess) ? p.guessAnonymousFunctions(result) : result;
  61 +}
  62 +
  63 +if (typeof module !== "undefined" && module.exports) {
  64 + module.exports = printStackTrace;
63 65 }
64 66  
65   -printStackTrace.implementation = function() {};
  67 +printStackTrace.implementation = function() {
  68 +};
66 69  
67 70 printStackTrace.implementation.prototype = {
68   - run: function(ex) {
69   - ex = ex ||
70   - (function() {
71   - try {
72   - var _err = __undef__ << 1;
73   - } catch (e) {
74   - return e;
75   - }
76   - })();
77   - // Use either the stored mode, or resolve it
78   - var mode = this._mode || this.mode(ex);
  71 + /**
  72 + * @param {Error} ex The error to create a stacktrace from (optional)
  73 + * @param {String} mode Forced mode (optional, mostly for unit tests)
  74 + */
  75 + run: function(ex, mode) {
  76 + ex = ex || this.createException();
  77 + // examine exception properties w/o debugger
  78 + //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);}
  79 + mode = mode || this.mode(ex);
79 80 if (mode === 'other') {
80 81 return this.other(arguments.callee);
81 82 } else {
82 83 return this[mode](ex);
83 84 }
84 85 },
85   -
  86 +
  87 + createException: function() {
  88 + try {
  89 + this.undef();
  90 + } catch (e) {
  91 + return e;
  92 + }
  93 + },
  94 +
86 95 /**
87   - * @return {String} mode of operation for the environment in question.
  96 + * Mode could differ for different exception, e.g.
  97 + * exceptions in Chrome may or may not have arguments or stack.
  98 + *
  99 + * @return {String} mode of operation for the exception
88 100 */
89 101 mode: function(e) {
90   - if (e['arguments']) {
91   - return (this._mode = 'chrome');
92   - } else if (window.opera && e.stacktrace) {
93   - return (this._mode = 'opera10');
  102 + if (e['arguments'] && e.stack) {
  103 + return 'chrome';
  104 + } else if (e.stack && e.sourceURL) {
  105 + return 'safari';
  106 + } else if (e.stack && e.number) {
  107 + return 'ie';
  108 + } else if (typeof e.message === 'string' && typeof window !== 'undefined' && window.opera) {
  109 + // e.message.indexOf("Backtrace:") > -1 -> opera
  110 + // !e.stacktrace -> opera
  111 + if (!e.stacktrace) {
  112 + return 'opera9'; // use e.message
  113 + }
  114 + // 'opera#sourceloc' in e -> opera9, opera10a
  115 + if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) {
  116 + return 'opera9'; // use e.message
  117 + }
  118 + // e.stacktrace && !e.stack -> opera10a
  119 + if (!e.stack) {
  120 + return 'opera10a'; // use e.stacktrace
  121 + }
  122 + // e.stacktrace && e.stack -> opera10b
  123 + if (e.stacktrace.indexOf("called from line") < 0) {
  124 + return 'opera10b'; // use e.stacktrace, format differs from 'opera10a'
  125 + }
  126 + // e.stacktrace && e.stack -> opera11
  127 + return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b'
  128 + } else if (e.stack && !e.fileName) {
  129 + // Chrome 27 does not have e.arguments as earlier versions,
  130 + // but still does not have e.fileName as Firefox
  131 + return 'chrome';
94 132 } else if (e.stack) {
95   - return (this._mode = 'firefox');
96   - } else if (window.opera && !('stacktrace' in e)) { //Opera 9-
97   - return (this._mode = 'opera');
  133 + return 'firefox';
98 134 }
99   - return (this._mode = 'other');
  135 + return 'other';
100 136 },
101 137  
102 138 /**
103 139 * Given a context, function name, and callback function, overwrite it so that it calls
104 140 * printStackTrace() first with a callback and then runs the rest of the body.
105   - *
  141 + *
106 142 * @param {Object} context of execution (e.g. window)
107 143 * @param {String} functionName to instrument
108   - * @param {Function} function to call with a stack trace on invocation
  144 + * @param {Function} callback function to call with a stack trace on invocation
109 145 */
110 146 instrumentFunction: function(context, functionName, callback) {
111 147 context = context || window;
112   - context['_old' + functionName] = context[functionName];
113   - context[functionName] = function() {
114   - callback.call(this, printStackTrace());
115   - return context['_old' + functionName].apply(this, arguments);
  148 + var original = context[functionName];
  149 + context[functionName] = function instrumented() {
  150 + callback.call(this, printStackTrace().slice(4));
  151 + return context[functionName]._instrumented.apply(this, arguments);
116 152 };
117   - context[functionName]._instrumented = true;
  153 + context[functionName]._instrumented = original;
118 154 },
119   -
  155 +
120 156 /**
121 157 * Given a context and function name of a function that has been
122 158 * instrumented, revert the function to it's original (non-instrumented)
... ... @@ -128,134 +164,207 @@ printStackTrace.implementation.prototype = {
128 164 deinstrumentFunction: function(context, functionName) {
129 165 if (context[functionName].constructor === Function &&
130 166 context[functionName]._instrumented &&
131   - context['_old' + functionName].constructor === Function) {
132   - context[functionName] = context['_old' + functionName];
  167 + context[functionName]._instrumented.constructor === Function) {
  168 + context[functionName] = context[functionName]._instrumented;
133 169 }
134 170 },
135   -
  171 +
136 172 /**
137 173 * Given an Error object, return a formatted Array based on Chrome's stack string.
138   - *
  174 + *
139 175 * @param e - Error object to inspect
140 176 * @return Array<String> of function calls, files and line numbers
141 177 */
142 178 chrome: function(e) {
143   - return e.stack.replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n');
  179 + var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, '').
  180 + replace(/^\s+(at eval )?at\s+/gm, '').
  181 + replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2').
  182 + replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n');
  183 + stack.pop();
  184 + return stack;
  185 + },
  186 +
  187 + /**
  188 + * Given an Error object, return a formatted Array based on Safari's stack string.
  189 + *
  190 + * @param e - Error object to inspect
  191 + * @return Array<String> of function calls, files and line numbers
  192 + */
  193 + safari: function(e) {
  194 + return e.stack.replace(/\[native code\]\n/m, '')
  195 + .replace(/^(?=\w+Error\:).*$\n/m, '')
  196 + .replace(/^@/gm, '{anonymous}()@')
  197 + .split('\n');
  198 + },
  199 +
  200 + /**
  201 + * Given an Error object, return a formatted Array based on IE's stack string.
  202 + *
  203 + * @param e - Error object to inspect
  204 + * @return Array<String> of function calls, files and line numbers
  205 + */
  206 + ie: function(e) {
  207 + var lineRE = /^.*at (\w+) \(([^\)]+)\)$/gm;
  208 + return e.stack.replace(/at Anonymous function /gm, '{anonymous}()@')
  209 + .replace(/^(?=\w+Error\:).*$\n/m, '')
  210 + .replace(lineRE, '$1@$2')
  211 + .split('\n');
144 212 },
145 213  
146 214 /**
147 215 * Given an Error object, return a formatted Array based on Firefox's stack string.
148   - *
  216 + *
149 217 * @param e - Error object to inspect
150 218 * @return Array<String> of function calls, files and line numbers
151 219 */
152 220 firefox: function(e) {
153   - return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n');
  221 + return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^[\(@]/gm, '{anonymous}()@').split('\n');
  222 + },
  223 +
  224 + opera11: function(e) {
  225 + var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/;
  226 + var lines = e.stacktrace.split('\n'), result = [];
  227 +
  228 + for (var i = 0, len = lines.length; i < len; i += 2) {
  229 + var match = lineRE.exec(lines[i]);
  230 + if (match) {
  231 + var location = match[4] + ':' + match[1] + ':' + match[2];
  232 + var fnName = match[3] || "global code";
  233 + fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON);
  234 + result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
  235 + }
  236 + }
  237 +
  238 + return result;
  239 + },
  240 +
  241 + opera10b: function(e) {
  242 + // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" +
  243 + // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" +
  244 + // "@file://localhost/G:/js/test/functional/testcase1.html:15"
  245 + var lineRE = /^(.*)@(.+):(\d+)$/;
  246 + var lines = e.stacktrace.split('\n'), result = [];
  247 +
  248 + for (var i = 0, len = lines.length; i < len; i++) {
  249 + var match = lineRE.exec(lines[i]);
  250 + if (match) {
  251 + var fnName = match[1]? (match[1] + '()') : "global code";
  252 + result.push(fnName + '@' + match[2] + ':' + match[3]);
  253 + }
  254 + }
  255 +
  256 + return result;
154 257 },
155 258  
156 259 /**
157 260 * Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
158   - *
  261 + *
159 262 * @param e - Error object to inspect
160 263 * @return Array<String> of function calls, files and line numbers
161 264 */
162   - opera10: function(e) {
163   - var stack = e.stacktrace;
164   - var lines = stack.split('\n'), ANON = '{anonymous}',
165   - lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len;
166   - for (i = 2, j = 0, len = lines.length; i < len - 2; i++) {
167   - if (lineRE.test(lines[i])) {
168   - var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2;
169   - var fnName = RegExp.$3;
170   - fnName = fnName.replace(/<anonymous function\:?\s?(\S+)?>/g, ANON);
171   - lines[j++] = fnName + '@' + location;
  265 + opera10a: function(e) {
  266 + // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n"
  267 + // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n"
  268 + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;
  269 + var lines = e.stacktrace.split('\n'), result = [];
  270 +
  271 + for (var i = 0, len = lines.length; i < len; i += 2) {
  272 + var match = lineRE.exec(lines[i]);
  273 + if (match) {
  274 + var fnName = match[3] || ANON;
  275 + result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
172 276 }
173 277 }
174   -
175   - lines.splice(j, lines.length - j);
176   - return lines;
  278 +
  279 + return result;
177 280 },
178   -
179   - // Opera 7.x-9.x only!
180   - opera: function(e) {
181   - var lines = e.message.split('\n'), ANON = '{anonymous}',
182   - lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i,
183   - i, j, len;
184   -
185   - for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
186   - //TODO: RegExp.exec() would probably be cleaner here
187   - if (lineRE.test(lines[i])) {
188   - lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, '');
  281 +
  282 + // Opera 7.x-9.2x only!
  283 + opera9: function(e) {
  284 + // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n"
  285 + // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n"
  286 + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i;
  287 + var lines = e.message.split('\n'), result = [];
  288 +
  289 + for (var i = 2, len = lines.length; i < len; i += 2) {
  290 + var match = lineRE.exec(lines[i]);
  291 + if (match) {
  292 + result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
189 293 }
190 294 }
191   -
192   - lines.splice(j, lines.length - j);
193   - return lines;
  295 +
  296 + return result;
194 297 },
195   -
196   - // Safari, IE, and others
  298 +
  299 + // Safari 5-, IE 9-, and others
197 300 other: function(curr) {
198   - var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i,
199   - stack = [], fn, args, maxStackSize = 10;
200   -
201   - while (curr && stack.length < maxStackSize) {
  301 + var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10;
  302 + while (curr && curr['arguments'] && stack.length < maxStackSize) {
202 303 fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
203   - args = Array.prototype.slice.call(curr['arguments']);
  304 + args = Array.prototype.slice.call(curr['arguments'] || []);
204 305 stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')';
205 306 curr = curr.caller;
206 307 }
207 308 return stack;
208 309 },
209   -
  310 +
210 311 /**
211   - * Given arguments array as a String, subsituting type names for non-string types.
  312 + * Given arguments array as a String, substituting type names for non-string types.
212 313 *
213   - * @param {Arguments} object
214   - * @return {Array} of Strings with stringified arguments
  314 + * @param {Arguments,Array} args
  315 + * @return {String} stringified arguments
215 316 */
216 317 stringifyArguments: function(args) {
  318 + var result = [];
  319 + var slice = Array.prototype.slice;
217 320 for (var i = 0; i < args.length; ++i) {
218 321 var arg = args[i];
219 322 if (arg === undefined) {
220   - args[i] = 'undefined';
  323 + result[i] = 'undefined';
221 324 } else if (arg === null) {
222   - args[i] = 'null';
  325 + result[i] = 'null';
223 326 } else if (arg.constructor) {
224 327 if (arg.constructor === Array) {
225 328 if (arg.length < 3) {
226   - args[i] = '[' + this.stringifyArguments(arg) + ']';
  329 + result[i] = '[' + this.stringifyArguments(arg) + ']';
227 330 } else {
228   - args[i] = '[' + this.stringifyArguments(Array.prototype.slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(Array.prototype.slice.call(arg, -1)) + ']';
  331 + result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']';
229 332 }
230 333 } else if (arg.constructor === Object) {
231   - args[i] = '#object';
  334 + result[i] = '#object';
232 335 } else if (arg.constructor === Function) {
233   - args[i] = '#function';
  336 + result[i] = '#function';
234 337 } else if (arg.constructor === String) {
235   - args[i] = '"' + arg + '"';
  338 + result[i] = '"' + arg + '"';
  339 + } else if (arg.constructor === Number) {
  340 + result[i] = arg;
236 341 }
237 342 }
238 343 }
239   - return args.join(',');
  344 + return result.join(',');
240 345 },
241   -
  346 +
242 347 sourceCache: {},
243   -
  348 +
244 349 /**
245   - * @return the text from a given URL.
  350 + * @return the text from a given URL
246 351 */
247 352 ajax: function(url) {
248 353 var req = this.createXMLHTTPObject();
249   - if (!req) {
250   - return;
  354 + if (req) {
  355 + try {
  356 + req.open('GET', url, false);
  357 + //req.overrideMimeType('text/plain');
  358 + //req.overrideMimeType('text/javascript');
  359 + req.send(null);
  360 + //return req.status == 200 ? req.responseText : '';
  361 + return req.responseText;
  362 + } catch (e) {
  363 + }
251 364 }
252   - req.open('GET', url, false);
253   - // REMOVED FOR JS TEST.
254   - //req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
255   - req.send('');
256   - return req.responseText;
  365 + return '';
257 366 },
258   -
  367 +
259 368 /**
260 369 * Try XHR methods in order and store XHR factory.
261 370 *
... ... @@ -279,7 +388,8 @@ printStackTrace.implementation.prototype = {
279 388 // Use memoization to cache the factory
280 389 this.createXMLHTTPObject = XMLHttpFactories[i];
281 390 return xmlhttp;
282   - } catch (e) {}
  391 + } catch (e) {
  392 + }
283 393 }
284 394 },
285 395  
... ... @@ -288,12 +398,12 @@ printStackTrace.implementation.prototype = {
288 398 * via Ajax).
289 399 *
290 400 * @param url <String> source url
291   - * @return False if we need a cross-domain request
  401 + * @return <Boolean> False if we need a cross-domain request
292 402 */
293 403 isSameDomain: function(url) {
294   - return url.indexOf(location.hostname) !== -1;
  404 + return typeof location !== "undefined" && url.indexOf(location.hostname) !== -1; // location may not be defined, e.g. when running from nodejs.
295 405 },
296   -
  406 +
297 407 /**
298 408 * Get source code from given URL if in the same domain.
299 409 *
... ... @@ -301,52 +411,78 @@ printStackTrace.implementation.prototype = {
301 411 * @return <Array> Array of source code lines
302 412 */
303 413 getSource: function(url) {
  414 + // TODO reuse source from script tags?
304 415 if (!(url in this.sourceCache)) {
305 416 this.sourceCache[url] = this.ajax(url).split('\n');
306 417 }
307 418 return this.sourceCache[url];
308 419 },
309   -
310   - guessFunctions: function(stack) {
  420 +
  421 + guessAnonymousFunctions: function(stack) {
311 422 for (var i = 0; i < stack.length; ++i) {
312   - var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
313   - var frame = stack[i], m = reStack.exec(frame);
314   - if (m) {
315   - var file = m[1], lineno = m[4]; //m[7] is character position in Chrome
316   - if (file && this.isSameDomain(file) && lineno) {
317   - var functionName = this.guessFunctionName(file, lineno);
318   - stack[i] = frame.replace('{anonymous}', functionName);
  423 + var reStack = /\{anonymous\}\(.*\)@(.*)/,
  424 + reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/,
  425 + frame = stack[i], ref = reStack.exec(frame);
  426 +
  427 + if (ref) {
  428 + var m = reRef.exec(ref[1]);
  429 + if (m) { // If falsey, we did not get any file/line information
  430 + var file = m[1], lineno = m[2], charno = m[3] || 0;
  431 + if (file && this.isSameDomain(file) && lineno) {
  432 + var functionName = this.guessAnonymousFunction(file, lineno, charno);
  433 + stack[i] = frame.replace('{anonymous}', functionName);
  434 + }
319 435 }
320 436 }
321 437 }
322 438 return stack;
323 439 },
324   -
325   - guessFunctionName: function(url, lineNo) {
  440 +
  441 + guessAnonymousFunction: function(url, lineNo, charNo) {
  442 + var ret;
326 443 try {
327   - return this.guessFunctionNameFromLines(lineNo, this.getSource(url));
  444 + ret = this.findFunctionName(this.getSource(url), lineNo);
328 445 } catch (e) {
329   - return 'getSource failed with url: ' + url + ', exception: ' + e.toString();
  446 + ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString();
330 447 }
  448 + return ret;
331 449 },
332   -
333   - guessFunctionNameFromLines: function(lineNo, source) {
334   - var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/;
335   - var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/;
336   - // Walk backwards from the first line in the function until we find the line which
337   - // matches the pattern above, which is the function definition
338   - var line = "", maxLines = 10;
  450 +
  451 + findFunctionName: function(source, lineNo) {
  452 + // FIXME findFunctionName fails for compressed source
  453 + // (more than one function on the same line)
  454 + // function {name}({args}) m[1]=name m[2]=args
  455 + var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/;
  456 + // {name} = function ({args}) TODO args capture
  457 + // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/
  458 + var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/;
  459 + // {name} = eval()
  460 + var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/;
  461 + // Walk backwards in the source lines until we find
  462 + // the line which matches one of the patterns above
  463 + var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos;
339 464 for (var i = 0; i < maxLines; ++i) {
340   - line = source[lineNo - i] + line;
341   - if (line !== undefined) {
342   - var m = reGuessFunction.exec(line);
  465 + // lineNo is 1-based, source[] is 0-based
  466 + line = source[lineNo - i - 1];
  467 + commentPos = line.indexOf('//');
  468 + if (commentPos >= 0) {
  469 + line = line.substr(0, commentPos);
  470 + }
  471 + // TODO check other types of comments? Commented code may lead to false positive
  472 + if (line) {
  473 + code = line + code;
  474 + m = reFunctionExpression.exec(code);
  475 + if (m && m[1]) {
  476 + return m[1];
  477 + }
  478 + m = reFunctionDeclaration.exec(code);
  479 + if (m && m[1]) {
  480 + //return m[1] + "(" + (m[2] || "") + ")";
  481 + return m[1];
  482 + }
  483 + m = reFunctionEvaluation.exec(code);
343 484 if (m && m[1]) {
344 485 return m[1];
345   - } else {
346   - m = reFunctionArgNames.exec(line);
347   - if (m && m[1]) {
348   - return m[1];
349   - }
350 486 }
351 487 }
352 488 }
... ... @@ -379,6 +515,11 @@ printStackTrace.implementation.prototype = {
379 515 '<project-root>{project_root}</project-root>' +
380 516 '<environment-name>{environment}</environment-name>' +
381 517 '</server-environment>' +
  518 + '<current-user>' +
  519 + '<id>{user_id}</id>' +
  520 + '<name>{user_name}</name>' +
  521 + '<email>{user_email}</email>' +
  522 + '</current-user>' +
382 523 '</notice>',
383 524 REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>',
384 525 REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>',
... ... @@ -411,9 +552,9 @@ printStackTrace.implementation.prototype = {
411 552 "rootDirectory": "{project_root}",
412 553 "action": "{request_action}",
413 554  
414   - "userId": "{}",
415   - "userName": "{}",
416   - "userEmail": "{}",
  555 + "userId": "{user_id}",
  556 + "userName": "{user_name}",
  557 + "userEmail": "{user_email}",
417 558 },
418 559 "environment": {},
419 560 //"session": "",
... ... @@ -683,6 +824,15 @@ printStackTrace.implementation.prototype = {
683 824 variable: 'outputFormat',
684 825 namespace: 'options'
685 826 }, {
  827 + methodName: 'setCurrentUser',
  828 + method: (function (value) {
  829 + for (var key in value) {
  830 + if (value.hasOwnProperty(key)) {
  831 + Config.xmlData['user_' + key] = value[key];
  832 + }
  833 + }
  834 + })
  835 + }, {
686 836 methodName: 'setTrackJQ',
687 837 variable: 'trackJQ',
688 838 namespace: 'options',
... ...
spec/controllers/api/v1/notices_controller_spec.rb
... ... @@ -17,7 +17,7 @@ describe Api::V1::NoticesController do
17 17  
18 18 it "should return JSON if JSON is requested" do
19 19 get :index, :auth_token => @user.authentication_token, :format => "json"
20   - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  20 + expect { JSON.load(response.body) }.not_to raise_error() #JSON::ParserError)
21 21 end
22 22  
23 23 it "should return XML if XML is requested" do
... ... @@ -27,7 +27,7 @@ describe Api::V1::NoticesController do
27 27  
28 28 it "should return JSON by default" do
29 29 get :index, :auth_token => @user.authentication_token
30   - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  30 + expect { JSON.load(response.body) }.not_to raise_error() #JSON::ParserError)
31 31 end
32 32  
33 33 describe "given a date range" do
... ...
spec/controllers/api/v1/problems_controller_spec.rb
... ... @@ -19,7 +19,7 @@ describe Api::V1::ProblemsController do
19 19  
20 20 it "should return JSON if JSON is requested" do
21 21 get :index, :auth_token => @user.authentication_token, :format => "json"
22   - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  22 + expect { JSON.load(response.body) }.not_to raise_error()#JSON::ParserError)
23 23 end
24 24  
25 25 it "should return XML if XML is requested" do
... ... @@ -29,7 +29,7 @@ describe Api::V1::ProblemsController do
29 29  
30 30 it "should return JSON by default" do
31 31 get :index, :auth_token => @user.authentication_token
32   - lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  32 + expect { JSON.load(response.body) }.not_to raise_error()#JSON::ParserError)
33 33 end
34 34  
35 35  
... ...
spec/controllers/apps_controller_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe AppsController do
4   - render_views
5 4  
6 5 it_requires_authentication
7 6 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete}
8 7  
9   -
10 8 describe "GET /apps" do
11 9 context 'when logged in as an admin' do
12 10 it 'finds all apps' do
13 11 sign_in Fabricate(:admin)
14 12 3.times { Fabricate(:app) }
15   - apps = App.all
16 13 get :index
17   - assigns(:apps).should == apps
  14 + controller.apps.should == App.all.sort.entries
18 15 end
19 16 end
20 17  
... ... @@ -27,8 +24,8 @@ describe AppsController do
27 24 Fabricate(:user_watcher, :user => user, :app => watched_app1)
28 25 Fabricate(:user_watcher, :user => user, :app => watched_app2)
29 26 get :index
30   - assigns(:apps).should include(watched_app1, watched_app2)
31   - assigns(:apps).should_not include(unwatched_app)
  27 + controller.apps.should include(watched_app1, watched_app2)
  28 + controller.apps.should_not include(unwatched_app)
32 29 end
33 30 end
34 31 end
... ... @@ -44,7 +41,7 @@ describe AppsController do
44 41  
45 42 it 'finds the app' do
46 43 get :show, :id => @app.id
47   - assigns(:app).should == @app
  44 + controller.app.should == @app
48 45 end
49 46  
50 47 it "should not raise errors for app with err without notices" do
... ... @@ -55,7 +52,6 @@ describe AppsController do
55 52 it "should list atom feed successfully" do
56 53 get :show, :id => @app.id, :format => "atom"
57 54 response.should be_success
58   - response.body.should match(@problem.message)
59 55 end
60 56  
61 57 context "pagination" do
... ... @@ -65,13 +61,13 @@ describe AppsController do
65 61  
66 62 it "should have default per_page value for user" do
67 63 get :show, :id => @app.id
68   - assigns(:problems).to_a.size.should == User::PER_PAGE
  64 + controller.problems.to_a.size.should == User::PER_PAGE
69 65 end
70 66  
71 67 it "should be able to override default per_page value" do
72 68 @user.update_attribute :per_page, 10
73 69 get :show, :id => @app.id
74   - assigns(:problems).to_a.size.should == 10
  70 + controller.problems.to_a.size.should == 10
75 71 end
76 72 end
77 73  
... ... @@ -85,14 +81,14 @@ describe AppsController do
85 81 context 'and no params' do
86 82 it 'shows only unresolved problems' do
87 83 get :show, :id => @app.id
88   - assigns(:problems).size.should == 1
  84 + controller.problems.size.should == 1
89 85 end
90 86 end
91 87  
92 88 context 'and all_problems=true params' do
93 89 it 'shows all errors' do
94 90 get :show, :id => @app.id, :all_errs => true
95   - assigns(:problems).size.should == 2
  91 + controller.problems.size.should == 2
96 92 end
97 93 end
98 94 end
... ... @@ -108,35 +104,35 @@ describe AppsController do
108 104 context 'no params' do
109 105 it 'shows errs for all environments' do
110 106 get :show, :id => @app.id
111   - assigns(:problems).size.should == 21
  107 + controller.problems.size.should == 21
112 108 end
113 109 end
114 110  
115 111 context 'environment production' do
116 112 it 'shows errs for just production' do
117 113 get :show, :id => @app.id, :environment => 'production'
118   - assigns(:problems).size.should == 6
  114 + controller.problems.size.should == 6
119 115 end
120 116 end
121 117  
122 118 context 'environment staging' do
123 119 it 'shows errs for just staging' do
124 120 get :show, :id => @app.id, :environment => 'staging'
125   - assigns(:problems).size.should == 5
  121 + controller.problems.size.should == 5
126 122 end
127 123 end
128 124  
129 125 context 'environment development' do
130 126 it 'shows errs for just development' do
131 127 get :show, :id => @app.id, :environment => 'development'
132   - assigns(:problems).size.should == 5
  128 + controller.problems.size.should == 5
133 129 end
134 130 end
135 131  
136 132 context 'environment test' do
137 133 it 'shows errs for just test' do
138 134 get :show, :id => @app.id, :environment => 'test'
139   - assigns(:problems).size.should == 5
  135 + controller.problems.size.should == 5
140 136 end
141 137 end
142 138 end
... ... @@ -149,7 +145,7 @@ describe AppsController do
149 145 watcher = Fabricate(:user_watcher, :app => app, :user => user)
150 146 sign_in user
151 147 get :show, :id => app.id
152   - assigns(:app).should == app
  148 + controller.app.should == app
153 149 end
154 150  
155 151 it 'does not find the app if the user is not watching it' do
... ... @@ -170,19 +166,19 @@ describe AppsController do
170 166 describe "GET /apps/new" do
171 167 it 'instantiates a new app with a prebuilt watcher' do
172 168 get :new
173   - assigns(:app).should be_a(App)
174   - assigns(:app).should be_new_record
175   - assigns(:app).watchers.should_not be_empty
  169 + controller.app.should be_a(App)
  170 + controller.app.should be_new_record
  171 + controller.app.watchers.should_not be_empty
176 172 end
177 173  
178 174 it "should copy attributes from an existing app" do
179 175 @app = Fabricate(:app, :name => "do not copy",
180 176 :github_repo => "test/example")
181 177 get :new, :copy_attributes_from => @app.id
182   - assigns(:app).should be_a(App)
183   - assigns(:app).should be_new_record
184   - assigns(:app).name.should be_blank
185   - assigns(:app).github_repo.should == "test/example"
  178 + controller.app.should be_a(App)
  179 + controller.app.should be_new_record
  180 + controller.app.name.should be_blank
  181 + controller.app.github_repo.should == "test/example"
186 182 end
187 183 end
188 184  
... ... @@ -190,7 +186,7 @@ describe AppsController do
190 186 it 'finds the correct app' do
191 187 app = Fabricate(:app)
192 188 get :edit, :id => app.id
193   - assigns(:app).should == app
  189 + controller.app.should == app
194 190 end
195 191 end
196 192  
... ... @@ -316,7 +312,6 @@ describe AppsController do
316 312  
317 313 @app.reload
318 314 @app.issue_tracker_configured?.should == false
319   - response.body.should match(/You must specify your/)
320 315 end
321 316 end
322 317 end
... ... @@ -326,12 +321,11 @@ describe AppsController do
326 321 describe "DELETE /apps/:id" do
327 322 before do
328 323 @app = Fabricate(:app)
329   - App.stub(:find).with(@app.id).and_return(@app)
330 324 end
331 325  
332 326 it "should find the app" do
333 327 delete :destroy, :id => @app.id
334   - assigns(:app).should == @app
  328 + controller.app.should == @app
335 329 end
336 330  
337 331 it "should destroy the app" do
... ...
spec/controllers/notices_controller_spec.rb
... ... @@ -6,7 +6,7 @@ describe NoticesController do
6 6 let(:notice) { Fabricate(:notice) }
7 7 let(:xml) { Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read }
8 8 let(:app) { Fabricate(:app) }
9   - let(:error_report) { mock(:valid? => true, :generate_notice! => true, :notice => notice) }
  9 + let(:error_report) { double(:valid? => true, :generate_notice! => true, :notice => notice) }
10 10  
11 11 context 'notices API' do
12 12 context "with all params" do
... ... @@ -46,7 +46,7 @@ describe NoticesController do
46 46 response.body.should match(%r{<url[^>]*>(.+)#{locate_path(notice.id)}</url>})
47 47 end
48 48 context "with an invalid API_KEY" do
49   - let(:error_report) { mock(:valid? => false) }
  49 + let(:error_report) { double(:valid? => false) }
50 50 it 'return 422' do
51 51 post :create, :format => :xml, :data => xml
52 52 expect(response.status).to eq 422
... ...
spec/controllers/problems_controller_spec.rb
... ... @@ -12,7 +12,7 @@ describe ProblemsController do
12 12  
13 13  
14 14 describe "GET /problems" do
15   - render_views
  15 + #render_views
16 16 context 'when logged in as an admin' do
17 17 before(:each) do
18 18 @user = Fabricate(:admin)
... ... @@ -20,18 +20,6 @@ describe ProblemsController do
20 20 @problem = Fabricate(:notice, :err => Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production"))).problem
21 21 end
22 22  
23   - it "should successfully list problems" do
24   - get :index
25   - response.should be_success
26   - response.body.gsub("&#8203;", "").should match(@problem.message)
27   - end
28   -
29   - it "should list atom feed successfully" do
30   - get :index, :format => "atom"
31   - response.should be_success
32   - response.body.should match(@problem.message)
33   - end
34   -
35 23 context "pagination" do
36 24 before(:each) do
37 25 35.times { Fabricate :err }
... ... @@ -39,13 +27,13 @@ describe ProblemsController do
39 27  
40 28 it "should have default per_page value for user" do
41 29 get :index
42   - assigns(:problems).to_a.size.should == User::PER_PAGE
  30 + controller.problems.to_a.size.should == User::PER_PAGE
43 31 end
44 32  
45 33 it "should be able to override default per_page value" do
46 34 @user.update_attribute :per_page, 10
47 35 get :index
48   - assigns(:problems).to_a.size.should == 10
  36 + controller.problems.to_a.size.should == 10
49 37 end
50 38 end
51 39  
... ... @@ -60,35 +48,35 @@ describe ProblemsController do
60 48 context 'no params' do
61 49 it 'shows problems for all environments' do
62 50 get :index
63   - assigns(:problems).size.should == 21
  51 + controller.problems.size.should == 21
64 52 end
65 53 end
66 54  
67 55 context 'environment production' do
68 56 it 'shows problems for just production' do
69 57 get :index, :environment => 'production'
70   - assigns(:problems).size.should == 6
  58 + controller.problems.size.should == 6
71 59 end
72 60 end
73 61  
74 62 context 'environment staging' do
75 63 it 'shows problems for just staging' do
76 64 get :index, :environment => 'staging'
77   - assigns(:problems).size.should == 5
  65 + controller.problems.size.should == 5
78 66 end
79 67 end
80 68  
81 69 context 'environment development' do
82 70 it 'shows problems for just development' do
83 71 get :index, :environment => 'development'
84   - assigns(:problems).size.should == 5
  72 + controller.problems.size.should == 5
85 73 end
86 74 end
87 75  
88 76 context 'environment test' do
89 77 it 'shows problems for just test' do
90 78 get :index, :environment => 'test'
91   - assigns(:problems).size.should == 5
  79 + controller.problems.size.should == 5
92 80 end
93 81 end
94 82 end
... ... @@ -101,8 +89,8 @@ describe ProblemsController do
101 89 watched_unresolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false))
102 90 watched_resolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true))
103 91 get :index
104   - assigns(:problems).should include(watched_unresolved_err.problem)
105   - assigns(:problems).should_not include(unwatched_err.problem, watched_resolved_err.problem)
  92 + controller.problems.should include(watched_unresolved_err.problem)
  93 + controller.problems.should_not include(unwatched_err.problem, watched_resolved_err.problem)
106 94 end
107 95 end
108 96 end
... ... @@ -115,10 +103,10 @@ describe ProblemsController do
115 103 3.times { problems << Fabricate(:err).problem }
116 104 3.times { problems << Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem }
117 105 Problem.should_receive(:ordered_by).and_return(
118   - mock('proxy', :page => mock('other_proxy', :per => problems))
  106 + double('proxy', :page => double('other_proxy', :per => problems))
119 107 )
120 108 get :index, :all_errs => true
121   - assigns(:problems).should == problems
  109 + controller.problems.should == problems
122 110 end
123 111 end
124 112  
... ... @@ -129,14 +117,14 @@ describe ProblemsController do
129 117 watched_unresolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false)
130 118 watched_resolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true)
131 119 get :index, :all_errs => true
132   - assigns(:problems).should include(watched_resolved_problem, watched_unresolved_problem)
133   - assigns(:problems).should_not include(unwatched_problem)
  120 + controller.problems.should include(watched_resolved_problem, watched_unresolved_problem)
  121 + controller.problems.should_not include(unwatched_problem)
134 122 end
135 123 end
136 124 end
137 125  
138 126 describe "GET /apps/:app_id/problems/:id" do
139   - render_views
  127 + #render_views
140 128  
141 129 context 'when logged in as an admin' do
142 130 before do
... ... @@ -145,12 +133,12 @@ describe ProblemsController do
145 133  
146 134 it "finds the app" do
147 135 get :show, :app_id => app.id, :id => err.problem.id
148   - assigns(:app).should == app
  136 + controller.app.should == app
149 137 end
150 138  
151 139 it "finds the problem" do
152 140 get :show, :app_id => app.id, :id => err.problem.id
153   - assigns(:problem).should == err.problem
  141 + controller.problem.should == err.problem
154 142 end
155 143  
156 144 it "successfully render page" do
... ... @@ -178,32 +166,6 @@ describe ProblemsController do
178 166 end
179 167 end
180 168  
181   - context "create issue button" do
182   - let(:button_matcher) { match(/create issue/) }
183   -
184   - it "should not exist for problem's app without issue tracker" do
185   - err = Fabricate :err
186   - get :show, :app_id => err.app.id, :id => err.problem.id
187   -
188   - response.body.should_not button_matcher
189   - end
190   -
191   - it "should exist for problem's app with issue tracker" do
192   - tracker = Fabricate(:lighthouse_tracker)
193   - err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app))
194   - get :show, :app_id => err.app.id, :id => err.problem.id
195   -
196   - response.body.should button_matcher
197   - end
198   -
199   - it "should not exist for problem with issue_link" do
200   - tracker = Fabricate(:lighthouse_tracker)
201   - err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app, :issue_link => "http://some.host"))
202   - get :show, :app_id => err.app.id, :id => err.problem.id
203   -
204   - response.body.should_not button_matcher
205   - end
206   - end
207 169 end
208 170  
209 171 context 'when logged in as a user' do
... ... @@ -217,7 +179,7 @@ describe ProblemsController do
217 179  
218 180 it 'finds the problem if the user is watching the app' do
219 181 get :show, :app_id => @watched_app.to_param, :id => @watched_err.problem.id
220   - assigns(:problem).should == @watched_err.problem
  182 + controller.problem.should == @watched_err.problem
221 183 end
222 184  
223 185 it 'raises a DocumentNotFound error if the user is not watching the app' do
... ... @@ -242,8 +204,8 @@ describe ProblemsController do
242 204 App.should_receive(:find).with(@problem.app.id).and_return(@problem.app)
243 205 @problem.app.problems.should_receive(:find).and_return(@problem.problem)
244 206 put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
245   - assigns(:app).should == @problem.app
246   - assigns(:problem).should == @problem.problem
  207 + controller.app.should == @problem.app
  208 + controller.problem.should == @problem.problem
247 209 end
248 210  
249 211 it "should resolve the issue" do
... ... @@ -269,7 +231,7 @@ describe ProblemsController do
269 231 end
270 232  
271 233 describe "POST /apps/:app_id/problems/:id/create_issue" do
272   - render_views
  234 + #render_views
273 235  
274 236 before(:each) do
275 237 sign_in Fabricate(:admin)
... ... @@ -379,31 +341,25 @@ describe ProblemsController do
379 341 @problem2 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => false)).problem
380 342 end
381 343  
382   - it "should apply to multiple problems" do
383   - post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
384   - assigns(:selected_problems).should == [@problem1, @problem2]
385   - end
386   -
387   - it "should require at least one problem" do
388   - post :resolve_several, :problems => []
389   - request.flash[:notice].should match(/You have not selected any/)
390   - end
391   -
392 344 context "POST /problems/merge_several" do
393 345 it "should require at least two problems" do
394 346 post :merge_several, :problems => [@problem1.id.to_s]
395   - request.flash[:notice].should match(/You must select at least two/)
  347 + request.flash[:notice].should eql I18n.t('controllers.problems.flash.need_two_errors_merge')
396 348 end
397 349  
398 350 it "should merge the problems" do
399   - lambda {
400   - post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
401   - assigns(:merged_problem).reload.errs.length.should == 2
402   - }.should change(Problem, :count).by(-1)
  351 + ProblemMerge.should_receive(:new).and_return(double(:merge => true))
  352 + post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
403 353 end
404 354 end
405 355  
406 356 context "POST /problems/unmerge_several" do
  357 +
  358 + it "should require at least one problem" do
  359 + post :unmerge_several, :problems => []
  360 + request.flash[:notice].should eql I18n.t('controllers.problems.flash.no_select_problem')
  361 + end
  362 +
407 363 it "should unmerge a merged problem" do
408 364 merged_problem = Problem.merge!(@problem1, @problem2)
409 365 merged_problem.errs.length.should == 2
... ... @@ -412,9 +368,16 @@ describe ProblemsController do
412 368 merged_problem.reload.errs.length.should == 1
413 369 }.should change(Problem, :count).by(1)
414 370 end
  371 +
415 372 end
416 373  
417 374 context "POST /problems/resolve_several" do
  375 +
  376 + it "should require at least one problem" do
  377 + post :resolve_several, :problems => []
  378 + request.flash[:notice].should eql I18n.t('controllers.problems.flash.no_select_problem')
  379 + end
  380 +
418 381 it "should resolve the issue" do
419 382 post :resolve_several, :problems => [@problem2.id.to_s]
420 383 @problem2.reload.resolved?.should == true
... ... @@ -428,10 +391,17 @@ describe ProblemsController do
428 391 it "should display a message about 2 errs" do
429 392 post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
430 393 flash[:success].should match(/2 errs have been resolved/)
  394 + controller.selected_problems.should == [@problem1, @problem2]
431 395 end
432 396 end
433 397  
434 398 context "POST /problems/unresolve_several" do
  399 +
  400 + it "should require at least one problem" do
  401 + post :unresolve_several, :problems => []
  402 + request.flash[:notice].should eql I18n.t('controllers.problems.flash.no_select_problem')
  403 + end
  404 +
435 405 it "should unresolve the issue" do
436 406 post :unresolve_several, :problems => [@problem1.id.to_s]
437 407 @problem1.reload.resolved?.should == false
... ...
spec/controllers/users/omniauth_callbacks_controller_spec.rb
... ... @@ -12,7 +12,7 @@ describe Users::OmniauthCallbacksController do
12 12 :credentials => { :token => token }
13 13 )
14 14 }
15   - @controller.stub!(:env).and_return(env)
  15 + @controller.stub(:env).and_return(env)
16 16 end
17 17  
18 18 context 'Linking a GitHub account to a signed in user' do
... ...
spec/controllers/users_controller_spec.rb
... ... @@ -207,7 +207,7 @@ describe UsersController do
207 207 context "DELETE /users/:id" do
208 208  
209 209 context "with a destroy success" do
210   - let(:user_destroy) { mock(:destroy => true) }
  210 + let(:user_destroy) { double(:destroy => true) }
211 211  
212 212 before {
213 213 UserDestroy.should_receive(:new).with(user).and_return(user_destroy)
... ...
spec/fabricators_spec.rb
... ... @@ -1,18 +0,0 @@
1   -require 'spec_helper'
2   -
3   -Fabrication::Config.fabricator_dir.each do |folder|
4   - Dir.glob(File.join(Rails.root, folder, '**', '*.rb')).each do |file|
5   - require file
6   - end
7   -end
8   -
9   -describe "Fabrication" do
10   - #TODO : when 1.8.7 drop support se directly Symbol#sort
11   - Fabrication::Fabricator.schematics.keys.sort_by(&:to_s).each do |fabricator_name|
12   - context "Fabricate(:#{fabricator_name})" do
13   - subject { Fabricate.build(fabricator_name) }
14   -
15   - it { should be_valid }
16   - end
17   - end
18   -end
spec/interactors/problem_destroy_spec.rb
... ... @@ -8,8 +8,8 @@ describe ProblemDestroy do
8 8 context "in unit way" do
9 9 let(:problem) {
10 10 problem = Problem.new
11   - problem.stub(:errs).and_return(mock(:criteria, :only => [err_1, err_2]))
12   - problem.stub(:comments).and_return(mock(:criteria, :only => [comment_1, comment_2]))
  11 + problem.stub(:errs).and_return(double(:criteria, :only => [err_1, err_2]))
  12 + problem.stub(:comments).and_return(double(:criteria, :only => [comment_1, comment_2]))
13 13 problem.stub(:delete)
14 14 problem
15 15 }
... ...
spec/interactors/problem_merge_spec.rb
... ... @@ -42,7 +42,7 @@ describe ProblemMerge do
42 42 end
43 43  
44 44 it 'update problem cache' do
45   - ProblemUpdaterCache.should_receive(:new).with(problem).and_return(mock(:update => true))
  45 + ProblemUpdaterCache.should_receive(:new).with(problem).and_return(double(:update => true))
46 46 problem_merge.merge
47 47 end
48 48  
... ...
spec/models/backtrace_spec.rb
... ... @@ -22,8 +22,8 @@ describe Backtrace do
22 22  
23 23 describe "find_or_create" do
24 24 subject { described_class.find_or_create(attributes) }
25   - let(:attributes) { mock :attributes }
26   - let(:backtrace) { mock :backtrace }
  25 + let(:attributes) { double :attributes }
  26 + let(:backtrace) { double :backtrace }
27 27  
28 28 before { described_class.stub(:new => backtrace) }
29 29  
... ... @@ -37,7 +37,7 @@ describe Backtrace do
37 37 end
38 38  
39 39 context "similar backtrace exist" do
40   - let(:similar_backtrace) { mock :similar_backtrace }
  40 + let(:similar_backtrace) { double :similar_backtrace }
41 41 before { backtrace.stub(:similar => similar_backtrace) }
42 42  
43 43 it { should == similar_backtrace }
... ...
spec/models/comment_observer_spec.rb
... ... @@ -10,7 +10,7 @@ describe CommentObserver do
10 10 it 'should send an email notification' do
11 11 Mailer.should_receive(:comment_notification).
12 12 with(comment).
13   - and_return(mock('email', :deliver => true))
  13 + and_return(double('email', :deliver => true))
14 14 comment.save
15 15 end
16 16 end
... ...
spec/models/deploy_observer_spec.rb
... ... @@ -5,7 +5,7 @@ describe DeployObserver do
5 5 context 'and the app should notify on deploys' do
6 6 it 'should send an email notification' do
7 7 Mailer.should_receive(:deploy_notification).
8   - and_return(mock('email', :deliver => true))
  8 + and_return(double('email', :deliver => true))
9 9 Fabricate(:deploy, :app => Fabricate(:app_with_watcher, :notify_on_deploys => true))
10 10 end
11 11 end
... ...
spec/models/fabricators_spec.rb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +require 'spec_helper'
  2 +
  3 +Fabrication::Config.fabricator_dir.each do |folder|
  4 + Dir.glob(File.join(Rails.root, folder, '**', '*.rb')).each do |file|
  5 + require file
  6 + end
  7 +end
  8 +
  9 +describe "Fabrication" do
  10 + #TODO : when 1.8.7 drop support se directly Symbol#sort
  11 + Fabrication::Fabricator.schematics.keys.sort_by(&:to_s).each do |fabricator_name|
  12 + context "Fabricate(:#{fabricator_name})" do
  13 + subject { Fabricate.build(fabricator_name) }
  14 +
  15 + it { should be_valid }
  16 + end
  17 + end
  18 +end
... ...
spec/models/issue_trackers/fogbugz_tracker_spec.rb
... ... @@ -9,7 +9,7 @@ describe IssueTrackers::FogbugzTracker do
9 9 number = 123
10 10 @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}"
11 11 response = "<response><token>12345</token><case><ixBug>123</ixBug></case></response>"
12   - http_mock = mock
  12 + http_mock = double
13 13 http_mock.should_receive(:new).and_return(http_mock)
14 14 http_mock.should_receive(:request).twice.and_return(response)
15 15 Fogbugz.adapter[:http] = http_mock
... ...
spec/models/notice_observer_spec.rb
... ... @@ -18,7 +18,7 @@ describe NoticeObserver do
18 18 it "sends an email notification after #{threshold} notice(s)" do
19 19 @err.problem.stub(:notices_count).and_return(threshold)
20 20 Mailer.should_receive(:err_notification).
21   - and_return(mock('email', :deliver => true))
  21 + and_return(double('email', :deliver => true))
22 22 Fabricate(:notice, :err => @err)
23 23 end
24 24 end
... ... @@ -38,7 +38,7 @@ describe NoticeObserver do
38 38 it "should send email notification after 1 notice since an error has been resolved" do
39 39 @err.problem.resolve!
40 40 Mailer.should_receive(:err_notification).
41   - and_return(mock('email', :deliver => true))
  41 + and_return(double('email', :deliver => true))
42 42 Fabricate(:notice, :err => @err)
43 43 end
44 44 end
... ... @@ -103,7 +103,7 @@ describe NoticeObserver do
103 103 Fabricate(:notice, :err => err)
104 104 end
105 105 end
106   -
  106 +
107 107 describe "should send a notification at desired intervals" do
108 108 let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service, :notify_at_notices => [1,2]))}
109 109 let(:backtrace) { Fabricate(:backtrace) }
... ...
spec/models/notification_service/campfire_service_spec.rb
... ... @@ -8,7 +8,7 @@ describe NotificationService::CampfireService do
8 8 problem = notice.problem
9 9  
10 10 #campy stubbing
11   - campy = mock('CampfireService')
  11 + campy = double('CampfireService')
12 12 Campy::Room.stub(:new).and_return(campy)
13 13 campy.stub(:speak) { true }
14 14  
... ...
spec/models/notification_service/gtalk_service_spec.rb
... ... @@ -9,7 +9,7 @@ describe NotificationService::GtalkService do
9 9 problem = notice.problem
10 10  
11 11 #gtalk stubbing
12   - gtalk = mock('GtalkService')
  12 + gtalk = double('GtalkService')
13 13 jid = double("jid")
14 14 message = double("message")
15 15 Jabber::JID.should_receive(:new).with(notification_service.subdomain).and_return(jid)
... ... @@ -19,13 +19,13 @@ describe NotificationService::GtalkService do
19 19 message_value = """#{problem.app.name.to_s}
20 20 http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}
21 21 #{notification_service.notification_description problem}"""
22   -
  22 +
23 23 Jabber::Message.should_receive(:new).with(notification_service.user_id, message_value).and_return(message)
24 24 Jabber::Message.should_receive(:new).with(notification_service.room_id, message_value).and_return(message)
25   -
  25 +
26 26 Jabber::MUC::SimpleMUCClient.should_receive(:new).and_return(gtalk)
27 27 gtalk.should_receive(:join).with(notification_service.room_id + "/errbit")
28   -
  28 +
29 29 #assert
30 30 gtalk.should_receive(:send).exactly(2).times.with(message)
31 31  
... ... @@ -43,7 +43,7 @@ http://#{Errbit::Config.host}/apps/#{@problem.app.id.to_s}
43 43 #{@notification_service.notification_description @problem}"""
44 44  
45 45 # gtalk stubbing
46   - @gtalk = mock('GtalkService')
  46 + @gtalk = double('GtalkService')
47 47 @gtalk.should_receive(:connect)
48 48 @gtalk.should_receive(:auth)
49 49 jid = double("jid")
... ... @@ -86,7 +86,7 @@ http://#{Errbit::Config.host}/apps/#{@problem.app.id.to_s}
86 86 @notification_service.room_id = ""
87 87 @notification_service.create_notification(@problem)
88 88 end
89   -
  89 +
90 90 end
91 91  
92 92 it "it should send a notification to room only" do
... ... @@ -97,7 +97,7 @@ http://#{Errbit::Config.host}/apps/#{@problem.app.id.to_s}
97 97 problem = notice.problem
98 98  
99 99 #gtalk stubbing
100   - gtalk = mock('GtalkService')
  100 + gtalk = double('GtalkService')
101 101 jid = double("jid")
102 102 message = double("message")
103 103 Jabber::JID.should_receive(:new).with(notification_service.subdomain).and_return(jid)
... ... @@ -107,11 +107,11 @@ http://#{Errbit::Config.host}/apps/#{@problem.app.id.to_s}
107 107 message_value = """#{problem.app.name.to_s}
108 108 http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}
109 109 #{notification_service.notification_description problem}"""
110   -
  110 +
111 111 Jabber::Message.should_receive(:new).with(notification_service.room_id, message_value).and_return(message)
112   -
  112 +
113 113 Jabber::MUC::SimpleMUCClient.should_receive(:new).and_return(gtalk)
114   - gtalk.should_receive(:join).with(notification_service.room_id + "/errbit")
  114 + gtalk.should_receive(:join).with(notification_service.room_id + "/errbit")
115 115  
116 116 notification_service.user_id = ""
117 117  
... ...
spec/models/notification_service/hoiio_service_spec.rb
... ... @@ -8,7 +8,7 @@ describe NotificationService::HoiioService do
8 8 problem = notice.problem
9 9  
10 10 # hoi stubbing
11   - sms = mock('HoiioService')
  11 + sms = double('HoiioService')
12 12 Hoi::SMS.stub(:new).and_return(sms)
13 13 sms.stub(:send) { true }
14 14  
... ...
spec/models/notification_service/pushover_service_spec.rb
... ... @@ -8,7 +8,7 @@ describe NotificationService::PushoverService do
8 8 problem = notice.problem
9 9  
10 10 # hoi stubbing
11   - notification = mock('PushoverService')
  11 + notification = double('PushoverService')
12 12 Rushover::Client.stub(:new).and_return(notification)
13 13 notification.stub(:notify) { true }
14 14  
... ...
spec/models/problem_spec.rb
... ... @@ -123,12 +123,12 @@ describe Problem do
123 123 it "should throw an err if it's not successful" do
124 124 problem = Fabricate(:problem)
125 125 problem.should_not be_resolved
126   - problem.stub!(:valid?).and_return(false)
  126 + problem.stub(:valid?).and_return(false)
127 127 ## update_attributes not test #valid? but #errors.any?
128 128 # https://github.com/mongoid/mongoid/blob/master/lib/mongoid/persistence.rb#L137
129 129 er = ActiveModel::Errors.new(problem)
130 130 er.add_on_blank(:resolved)
131   - problem.stub!(:errors).and_return(er)
  131 + problem.stub(:errors).and_return(er)
132 132 problem.should_not be_valid
133 133 lambda {
134 134 problem.resolve!
... ... @@ -152,7 +152,6 @@ describe Problem do
152 152 end
153 153 end
154 154  
155   -
156 155 context "Scopes" do
157 156 context "resolved" do
158 157 it 'only finds resolved Problems' do
... ...
spec/views/apps/edit.html.haml_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe "apps/edit.html.haml" do
  4 + let(:app) { stub_model(App) }
4 5 before do
5   - app = stub_model(App)
6   - assign :app, app
  6 + view.stub(:app).and_return(app)
7 7 controller.stub(:current_user) { stub_model(User) }
8 8 end
9 9  
... ... @@ -19,5 +19,19 @@ describe &quot;apps/edit.html.haml&quot; do
19 19 end
20 20  
21 21 end
  22 +
  23 + context "with unvalid app" do
  24 + let(:app) {
  25 + app = stub_model(App)
  26 + app.errors.add(:base,'You must specify your')
  27 + app
  28 + }
  29 +
  30 + it 'see the error' do
  31 + render
  32 + rendered.should match(/You must specify your/)
  33 + end
  34 + end
  35 +
22 36 end
23 37  
... ...
spec/views/apps/index.html.haml_spec.rb
... ... @@ -3,7 +3,7 @@ require &#39;spec_helper&#39;
3 3 describe "apps/index.html.haml" do
4 4 before do
5 5 app = stub_model(App, :deploys => [stub_model(Deploy, :created_at => Time.now, :revision => "123456789abcdef")])
6   - assign :apps, [app]
  6 + view.stub(:apps).and_return([app])
7 7 controller.stub(:current_user) { stub_model(User) }
8 8 end
9 9  
... ...
spec/views/apps/new.html.haml_spec.rb 0 → 100644
... ... @@ -0,0 +1,37 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "apps/new.html.haml" do
  4 + let(:app) { stub_model(App) }
  5 + before do
  6 + view.stub(:app).and_return(app)
  7 + controller.stub(:current_user) { stub_model(User) }
  8 + end
  9 +
  10 + describe "content_for :action_bar" do
  11 + def action_bar
  12 + view.content_for(:action_bar)
  13 + end
  14 +
  15 + it "should confirm the 'cancel' link" do
  16 + render
  17 +
  18 + action_bar.should have_selector('a.button', :text => 'cancel')
  19 + end
  20 +
  21 + end
  22 +
  23 + context "with unvalid app" do
  24 + let(:app) {
  25 + app = stub_model(App)
  26 + app.errors.add(:base,'You must specify your')
  27 + app
  28 + }
  29 +
  30 + it 'see the error' do
  31 + render
  32 + rendered.should match(/You must specify your/)
  33 + end
  34 + end
  35 +
  36 +end
  37 +
... ...
spec/views/apps/show.atom.builder_spec.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "apps/show.atom.builder" do
  4 + let(:app) { stub_model(App) }
  5 + let(:problems) { [
  6 + stub_model(Problem, :message => 'foo', :app => app)
  7 + ]}
  8 +
  9 + before do
  10 + view.stub(:app).and_return(app)
  11 + view.stub(:problems).and_return(problems)
  12 + end
  13 +
  14 + context "with errs" do
  15 + it 'see the errs message' do
  16 + render
  17 + expect(rendered).to match(problems.first.message)
  18 + end
  19 + end
  20 +
  21 +end
... ...
spec/views/apps/show.html.haml_spec.rb 0 → 100644
... ... @@ -0,0 +1,32 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "apps/show.html.haml" do
  4 + let(:app) { stub_model(App) }
  5 + before do
  6 + view.stub(:app).and_return(app)
  7 + view.stub(:all_errs).and_return(false)
  8 + view.stub(:deploys).and_return([])
  9 + controller.stub(:current_user) { stub_model(User) }
  10 + end
  11 +
  12 + describe "content_for :action_bar" do
  13 + def action_bar
  14 + view.content_for(:action_bar)
  15 + end
  16 +
  17 + it "should confirm the 'cancel' link" do
  18 + render
  19 +
  20 + action_bar.should have_selector('a.button', :text => 'all errs')
  21 + end
  22 +
  23 + end
  24 +
  25 + context "without errs" do
  26 + it 'see no errs' do
  27 + render
  28 + rendered.should match(/No errs have been/)
  29 + end
  30 + end
  31 +end
  32 +
... ...
spec/views/problems/index.atom.builder_spec.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "problems/index.atom.builder" do
  4 +
  5 + it 'display problem message' do
  6 + app = App.new(:new_record => false)
  7 + view.stub(:problems).and_return([Problem.new(
  8 + :message => 'foo',
  9 + :new_record => false, :app => app), Problem.new(:new_record => false, :app => app)])
  10 + render
  11 + rendered.should match('foo')
  12 + end
  13 +
  14 +end
... ...
spec/views/problems/index.html.haml_spec.rb 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "problems/index.html.haml" do
  4 + let(:problem_1) { Fabricate(:problem) }
  5 + let(:problem_2) { Fabricate(:problem, :app => problem_1.app) }
  6 +
  7 + before do
  8 + # view.stub(:app).and_return(problem.app)
  9 + view.stub(:selected_problems).and_return([])
  10 + view.stub(:problems).and_return(Kaminari.paginate_array([problem_1, problem_2]).page(1).per(10))
  11 + view.stub(:params_sort).and_return('asc')
  12 + controller.stub(:current_user) { Fabricate(:user) }
  13 + end
  14 +
  15 + describe "with problem" do
  16 + before { problem_1 && problem_2 }
  17 +
  18 + it 'should works' do
  19 + render
  20 + rendered.should have_selector('div#problem_table.problem_table')
  21 + end
  22 + end
  23 +
  24 +end
  25 +
... ...
spec/views/problems/show.html.haml_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe "problems/show.html.haml" do
  4 + let(:problem) { Fabricate(:problem) }
  5 + let(:comment) { Fabricate(:comment) }
  6 +
4 7 before do
5   - problem = Fabricate(:problem)
6   - comment = Fabricate(:comment)
7   - assign :problem, problem
  8 + view.stub(:app).and_return(problem.app)
  9 + view.stub(:problem).and_return(problem)
  10 +
8 11 assign :comment, comment
9   - assign :app, problem.app
10 12 assign :notices, problem.notices.page(1).per(1)
11 13 assign :notice, problem.notices.first
  14 +
12 15 controller.stub(:current_user) { Fabricate(:user) }
13 16 end
14 17  
15 18 def with_issue_tracker(tracker, problem)
16 19 problem.app.issue_tracker = tracker.new :api_token => "token token token", :project_id => "1234"
17   - assign :problem, problem
18   - assign :app, problem.app
  20 + view.stub(:problem).and_return(problem)
  21 + view.stub(:app).and_return(problem.app)
19 22 end
20 23  
21 24 describe "content_for :action_bar" do
... ... @@ -54,8 +57,8 @@ describe &quot;problems/show.html.haml&quot; do
54 57 it "should link 'up' to app_problems_path if HTTP_REFERER isn't set'" do
55 58 controller.request.env['HTTP_REFERER'] = nil
56 59 problem = Fabricate(:problem_with_comments)
57   - assign :problem, problem
58   - assign :app, problem.app
  60 + view.stub(:problem).and_return(problem)
  61 + view.stub(:app).and_return(problem.app)
59 62 render
60 63  
61 64 action_bar.should have_selector("span a.up[href='#{app_problems_path(problem.app)}']", :text => 'up')
... ... @@ -67,8 +70,8 @@ describe &quot;problems/show.html.haml&quot; do
67 70 controller.stub(:current_user) { user }
68 71  
69 72 problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo"))
70   - assign :problem, problem
71   - assign :app, problem.app
  73 + view.stub(:problem).and_return(problem)
  74 + view.stub(:app).and_return(problem.app)
72 75 render
73 76  
74 77 action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue')
... ... @@ -77,12 +80,54 @@ describe &quot;problems/show.html.haml&quot; do
77 80 it 'should allow creating issue for github if application has a github tracker' do
78 81 problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo"))
79 82 with_issue_tracker(GithubIssuesTracker, problem)
80   - assign :problem, problem
81   - assign :app, problem.app
  83 + view.stub(:problem).and_return(problem)
  84 + view.stub(:app).and_return(problem.app)
82 85 render
83 86  
84 87 action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue')
85 88 end
  89 +
  90 + context "without issue tracker associate on app" do
  91 + let(:problem){ Problem.new(:new_record => false, :app => app) }
  92 + let(:app) { App.new(:new_record => false) }
  93 +
  94 + it 'not see link to create issue' do
  95 + view.stub(:problem).and_return(problem)
  96 + view.stub(:app).and_return(problem.app)
  97 + render
  98 + expect(view.content_for(:action_bar)).to_not match(/create issue/)
  99 + end
  100 +
  101 + end
  102 +
  103 + context "with lighthouse tracker on app" do
  104 + let(:app) { App.new(:new_record => false, :issue_tracker => tracker ) }
  105 + let(:tracker) {
  106 + IssueTrackers::LighthouseTracker.new(:project_id => 'x')
  107 + }
  108 + context "with problem without issue link" do
  109 + let(:problem){ Problem.new(:new_record => false, :app => app) }
  110 + it 'not see link if no issue tracker' do
  111 + view.stub(:problem).and_return(problem)
  112 + view.stub(:app).and_return(problem.app)
  113 + render
  114 + expect(view.content_for(:action_bar)).to match(/create issue/)
  115 + end
  116 +
  117 + end
  118 +
  119 + context "with problem with issue link" do
  120 + let(:problem){ Problem.new(:new_record => false, :app => app, :issue_link => 'http://foo') }
  121 +
  122 + it 'not see link if no issue tracker' do
  123 + view.stub(:problem).and_return(problem)
  124 + view.stub(:app).and_return(problem.app)
  125 + render
  126 + expect(view.content_for(:action_bar)).to_not match(/create issue/)
  127 + end
  128 + end
  129 +
  130 + end
86 131 end
87 132 end
88 133  
... ... @@ -94,8 +139,8 @@ describe &quot;problems/show.html.haml&quot; do
94 139  
95 140 it 'should display comments and new comment form when no issue tracker' do
96 141 problem = Fabricate(:problem_with_comments)
97   - assign :problem, problem
98   - assign :app, problem.app
  142 + view.stub(:problem).and_return(problem)
  143 + view.stub(:app).and_return(problem.app)
99 144 render
100 145  
101 146 view.content_for(:comments).should include('Test comment')
... ...
spec/views/problems/show.ics.haml_spec.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "problems/show.html.ics" do
  4 + let(:problem) { Fabricate(:problem) }
  5 + before do
  6 + view.stub(:problem).and_return(problem)
  7 + end
  8 +
  9 + it 'should work' do
  10 + render :template => 'problems/show', :formats => [:ics], :handlers => [:haml]
  11 + end
  12 +
  13 +
  14 +end
... ...