Commit ba398235a740b4902fc2993b45a8e2f82b4530dd

Authored by Nathan Broadbent
2 parents 2525542a 2baa0302
Exists in master and in 1 other branch production

Merge branch 'notifications' of https://github.com/amaabca/errbit into amaabca-notifications

Conflicts:
	Gemfile.lock
Gemfile
... ... @@ -5,6 +5,9 @@ gem 'rails', '3.2.8'
5 5 gem 'nokogiri'
6 6 gem 'mongoid', '~> 2.4.10'
7 7  
  8 +# force SSL
  9 +gem 'rack-ssl', :require => 'rack/ssl'
  10 +
8 11 gem 'haml'
9 12 gem 'htmlentities', "~> 4.3.0"
10 13  
... ... @@ -30,6 +33,7 @@ gem 'kaminari'
30 33 gem 'rack-ssl-enforcer'
31 34 gem 'fabrication', "~> 1.3.0" # Both for tests, and loading demo data
32 35 gem 'rails_autolink', '~> 1.0.9'
  36 +gem 'campy'
33 37  
34 38 platform :ruby do
35 39 gem 'mongo', '= 1.6.2'
... ... @@ -45,7 +49,6 @@ group :development, :test do
45 49 gem 'webmock', :require => false
46 50 unless ENV["CI"]
47 51 gem 'ruby-debug', :platform => :mri_18
48   - gem 'debugger', :platform => :mri_19
49 52 end
50 53 # gem 'rpm_contrib'
51 54 # gem 'newrelic_rpm'
... ...
Gemfile.lock
... ... @@ -40,6 +40,8 @@ GEM
40 40 bson_ext (1.6.2)
41 41 bson (~> 1.6.2)
42 42 builder (3.0.3)
  43 + campy (0.1.3)
  44 + multi_json (~> 1.0)
43 45 capistrano (2.13.3)
44 46 highline
45 47 net-scp (>= 1.0.0)
... ... @@ -62,13 +64,6 @@ GEM
62 64 rdoc
63 65 daemons (1.1.8)
64 66 database_cleaner (0.6.7)
65   - debugger (1.2.0)
66   - columnize (>= 0.3.1)
67   - debugger-linecache (~> 1.1.1)
68   - debugger-ruby_core_source (~> 1.1.3)
69   - debugger-linecache (1.1.2)
70   - debugger-ruby_core_source (>= 1.1.1)
71   - debugger-ruby_core_source (1.1.3)
72 67 devise (1.5.3)
73 68 bcrypt-ruby (~> 3.0)
74 69 orm_adapter (~> 0.0.3)
... ... @@ -289,10 +284,10 @@ DEPENDENCIES
289 284 actionmailer_inline_css (~> 1.3.0)
290 285 bson (= 1.6.2)
291 286 bson_ext (= 1.6.2)
  287 + campy
292 288 capistrano
293 289 capybara
294 290 database_cleaner (~> 0.6.0)
295   - debugger
296 291 devise (~> 1.5.3)
297 292 email_spec
298 293 execjs
... ... @@ -313,6 +308,7 @@ DEPENDENCIES
313 308 omniauth-github
314 309 oruen_redmine_client
315 310 pivotal-tracker
  311 + rack-ssl
316 312 rack-ssl-enforcer
317 313 rails (= 3.2.8)
318 314 rails_autolink (~> 1.0.9)
... ...
app/assets/images/campfire_create.png 0 → 100644

3.19 KB

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

3.19 KB

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

2.8 KB

app/assets/javascripts/form.js
... ... @@ -8,6 +8,9 @@ $(function(){
8 8 if($('div.issue_tracker.nested').length)
9 9 activateTypeSelector('issue_tracker', 'tracker_params');
10 10  
  11 + if($('div.notification_service.nested').length)
  12 + activateTypeSelector('notification_service', 'notification_params');
  13 +
11 14 $('body').addClass('has-js');
12 15 $('.label_radio').click(function(){
13 16 activateLabelIcons();
... ...
app/assets/stylesheets/application.css.erb
... ... @@ -3,6 +3,7 @@
3 3 *= require jquery.alerts
4 4 *= require errbit
5 5 *= require issue_tracker_icons
  6 + *= require notification_service_icons
6 7 *= require_self
7 8 */
8 9  
... ...
app/assets/stylesheets/errbit.css
... ... @@ -535,10 +535,11 @@ a.button.active {
535 535 display: inline-block;
536 536 }
537 537  
538   -/* Watchers and Issue Tracker Forms */
539   -div.watcher.nested .watcher_params, div.issue_tracker.nested .tracker_params {
  538 +/* Watchers / Issue Tracker / Notification Forms */
  539 +div.watcher.nested .watcher_params, div.issue_tracker.nested .tracker_params, div.notification_service.nested .notification_params {
540 540 display: none;
541 541 }
  542 +
542 543 div.nested .chosen {
543 544 display: block !important;
544 545 }
... ... @@ -546,35 +547,35 @@ div.nested .choose {
546 547 margin-bottom: 0.5em;
547 548 }
548 549  
549   -div.issue_tracker.nested .choose {
  550 +div.issue_tracker.nested .choose, div.notification_service.nested .choose {
550 551 background-color: #ebebeb;
551 552 border: 1px solid #dddddd;
552 553 margin: 0 0 15px;
553 554 padding: 12px;
554 555 }
555   -div.issue_tracker.nested img {
  556 +div.issue_tracker.nested img, div.notification_service.nested img {
556 557 vertical-align: middle;
557 558 }
558 559  
559 560 /* Icons for Issue Tracker Radio Buttons */
560   -div.issue_tracker.nested label.label_radio {
  561 +div.issue_tracker.nested label.label_radio, div.notification_service.nested label.label_radio {
561 562 color: #929292;
562 563 padding-left: 33px;
563 564 margin-bottom: 6px;
564 565 margin-right: 8px;
565 566 line-height: 30px;
566 567 }
567   -div.issue_tracker.nested .choose {
  568 +div.issue_tracker.nested .choose, div.notification_service.nested .choose {
568 569 padding-bottom: 6px;
569 570 }
570   -div.issue_tracker.nested label.label_radio:hover {
  571 +div.issue_tracker.nested label.label_radio:hover, div.notification_service.nested label.label_radio:hover {
571 572 color: #696969;
572 573 }
573   -div.issue_tracker.nested .label_radio input {
  574 +div.issue_tracker.nested .label_radio input, div.notification_service.nested .label_radio input {
574 575 position: absolute; left: -9999px;
575 576 }
576 577  
577   -div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover {
  578 +div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover, div.notification_service.nested label.r_on, div.notification_service.nested label.r_on:hover {
578 579 color: #191919;
579 580 }
580 581  
... ...
app/assets/stylesheets/issue_tracker_icons.css.erb
1 1 /* Issue Tracker inactive, select, create and goto icons */
2 2 <% trackers = IssueTracker.subclasses.map{|t| t.label } << 'none' %>
  3 +
3 4 <% trackers.each do |tracker| %>
4 5 div.issue_tracker.nested label.<%= tracker %> {
5 6 background: url(/assets/<%= tracker %>_inactive.png) no-repeat;
... ... @@ -14,3 +15,4 @@ div.issue_tracker.nested label.r_on.&lt;%= tracker %&gt; {
14 15 background: transparent url(/assets/<%= tracker %>_goto.png) 6px 5px no-repeat;
15 16 }
16 17 <% end %>
  18 +
... ...
app/assets/stylesheets/notification_service_icons.css.erb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 + /* Notification Service inactive, select, create and goto icons */
  2 +<% notification_services = NotificationService.subclasses.map{|t| t.label } << 'none' %>
  3 +
  4 +<% notification_services.each do |notification_service| %>
  5 +div.notification_service.nested label.<%= notification_service %> {
  6 + background: url(/assets/<%= notification_service %>_inactive.png) no-repeat;
  7 +}
  8 +div.notification_service.nested label.r_on.<%= notification_service %> {
  9 + background: url(/assets/<%= notification_service %>_create.png) no-repeat;
  10 +}
  11 +#action-bar a.<%= notification_service %>_create {
  12 + background: transparent url(/assets/<%= notification_service %>_create.png) 6px 5px no-repeat;
  13 +}
  14 +#action-bar a.<%= notification_service %>_goto {
  15 + background: transparent url(/assets/<%= notification_service %>_goto.png) 6px 5px no-repeat;
  16 +}
  17 +<% end %>
  18 +
... ...
app/controllers/apps_controller.rb
... ... @@ -29,12 +29,14 @@ class AppsController &lt; InheritedResources::Base
29 29 def create
30 30 @app = App.new(params[:app])
31 31 initialize_subclassed_issue_tracker
  32 + initialize_subclassed_notification_service
32 33 create!
33 34 end
34 35  
35 36 def update
36 37 @app = resource
37 38 initialize_subclassed_issue_tracker
  39 + initialize_subclassed_notification_service
38 40 update!
39 41 end
40 42  
... ... @@ -70,6 +72,7 @@ class AppsController &lt; InheritedResources::Base
70 72 end
71 73  
72 74 def initialize_subclassed_issue_tracker
  75 + # set the app's issue tracker
73 76 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type]
74 77 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type)
75 78 @app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
... ... @@ -77,6 +80,15 @@ class AppsController &lt; InheritedResources::Base
77 80 end
78 81 end
79 82  
  83 + def initialize_subclassed_notification_service
  84 + # set the app's notification service
  85 + if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
  86 + if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
  87 + @app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
  88 + end
  89 + end
  90 + end
  91 +
80 92 def begin_of_association_chain
81 93 # Filter the @apps collection to apps watched by the current user, unless user is an admin.
82 94 # If user is an admin, then no filter is applied, and all apps are shown.
... ... @@ -90,6 +102,7 @@ class AppsController &lt; InheritedResources::Base
90 102 def plug_params app
91 103 app.watchers.build if app.watchers.none?
92 104 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured?
  105 + app.notification_service = NotificationService.new unless app.notification_service_configured?
93 106 app.copy_attributes_from(params[:copy_attributes_from]) if params[:copy_attributes_from]
94 107 end
95 108  
... ...
app/helpers/apps_helper.rb
... ... @@ -16,6 +16,11 @@ module AppsHelper
16 16 @any_github_repos
17 17 end
18 18  
  19 + def any_notification_services?
  20 + detect_any_apps_with_attributes unless @any_notification_services
  21 + @any_notification_services
  22 + end
  23 +
19 24 def any_issue_trackers?
20 25 detect_any_apps_with_attributes unless @any_issue_trackers
21 26 @any_issue_trackers
... ... @@ -29,11 +34,12 @@ module AppsHelper
29 34 private
30 35  
31 36 def detect_any_apps_with_attributes
32   - @any_github_repos = @any_issue_trackers = @any_deploys = false
  37 + @any_github_repos = @any_issue_trackers = @any_deploys = @any_notification_services = false
33 38 @apps.each do |app|
34 39 @any_github_repos ||= app.github_repo?
35 40 @any_issue_trackers ||= app.issue_tracker_configured?
36 41 @any_deploys ||= !!app.last_deploy_at
  42 + @any_notification_services ||= app.notification_service_configured?
37 43 end
38 44 end
39 45 end
... ...
app/models/app.rb
... ... @@ -17,6 +17,8 @@ class App
17 17 embeds_many :watchers
18 18 embeds_many :deploys
19 19 embeds_one :issue_tracker
  20 + embeds_one :notification_service
  21 +
20 22 has_many :problems, :inverse_of => :app, :dependent => :destroy
21 23  
22 24 before_validation :generate_api_key, :on => :create
... ... @@ -33,7 +35,8 @@ class App
33 35 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
34 36 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
35 37 :reject_if => proc { |attrs| !IssueTracker.subclasses.map(&:to_s).include?(attrs[:type].to_s) }
36   -
  38 + accepts_nested_attributes_for :notification_service, :allow_destroy => true,
  39 + :reject_if => proc { |attrs| !NotificationService.subclasses.map(&:to_s).include?(attrs[:type].to_s) }
37 40  
38 41 # Processes a new error report.
39 42 #
... ... @@ -121,6 +124,11 @@ class App
121 124 !!(issue_tracker && issue_tracker.class < IssueTracker && issue_tracker.project_id.present?)
122 125 end
123 126  
  127 + def notification_service_configured?
  128 + !!(notification_service && notification_service.class < NotificationService && notification_service.api_token.present?)
  129 + end
  130 +
  131 +
124 132 def notification_recipients
125 133 if notify_all_users
126 134 (User.all.map(&:email).reject(&:blank?) + watchers.map(&:address)).uniq
... ... @@ -137,7 +145,7 @@ class App
137 145 self.send("#{k}=", copy_app.send(k))
138 146 end
139 147 # Clone the embedded objects that can be changed via apps/edit (ignore errs & deploys, etc.)
140   - %w(watchers issue_tracker).each do |relation|
  148 + %w(watchers issue_tracker notification_service).each do |relation|
141 149 if obj = copy_app.send(relation)
142 150 self.send("#{relation}=", obj.is_a?(Array) ? obj.map(&:clone) : obj.clone)
143 151 end
... ...
app/models/issue_tracker.rb
... ... @@ -14,6 +14,7 @@ class IssueTracker
14 14 field :username, :type => String
15 15 field :password, :type => String
16 16 field :ticket_properties, :type => String
  17 + field :subdomain, :type => String
17 18  
18 19 validate :check_params
19 20  
... ...
app/models/notice_observer.rb
... ... @@ -4,6 +4,11 @@ class NoticeObserver &lt; Mongoid::Observer
4 4 def after_create notice
5 5 return unless should_notify? notice
6 6  
  7 + # if the app has a notficiation service, fire it off
  8 + unless notice.app.notification_service.nil?
  9 + notice.app.notification_service.create_notification(notice.problem)
  10 + end
  11 +
7 12 Mailer.err_notification(notice).deliver
8 13 end
9 14  
... ... @@ -15,5 +20,4 @@ class NoticeObserver &lt; Mongoid::Observer
15 20 (Errbit::Config.per_app_email_at_notices && app.email_at_notices || Errbit::Config.email_at_notices).include?(notice.problem.notices_count) &&
16 21 app.notification_recipients.any?
17 22 end
18   -
19 23 end
... ...
app/models/notification_service.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +class NotificationService
  2 + include Mongoid::Document
  3 +
  4 + field :room_id, :type => String
  5 + field :api_token, :type => String
  6 + field :subdomain, :type => String
  7 +
  8 + embedded_in :app, :inverse_of => :notification_service
  9 +
  10 + validate :check_params
  11 +
  12 + # Subclasses are responsible for overwriting this method.
  13 + def check_params; true; end
  14 +
  15 + def notification_description(problem)
  16 + "[#{ problem.environment }][#{ problem.where }] #{problem.message.to_s.truncate(100)}"
  17 + end
  18 +
  19 + # Allows us to set the issue tracker class from a single form.
  20 + def type; self._type; end
  21 + def type=(t); self._type=t; end
  22 +
  23 + # Retrieve tracker label from either class or instance.
  24 + Label = ''
  25 + def self.label; self::Label; end
  26 + def label; self.class.label; end
  27 +end
... ...
app/models/notification_services/campfire_service.rb 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +class NotificationService::CampfireService < NotificationService
  2 + Label = "campfire"
  3 + Fields = [
  4 + [:subdomain, {
  5 + :placeholder => "Campfire Subdomain"
  6 + }],
  7 + [:api_token, {
  8 + :placeholder => "API Token"
  9 + }],
  10 + [:room_id, {
  11 + :placeholder => "Room ID",
  12 + :label => "Room ID"
  13 + }],
  14 + ]
  15 +
  16 + def check_params
  17 + if Fields.detect {|f| self[f[0]].blank? }
  18 + errors.add :base, 'You must specify your Campfire Subdomain, API token and Room ID'
  19 + end
  20 + end
  21 +
  22 + def create_notification(problem)
  23 + # build the campfire client
  24 + campy = Campy::Room.new(:account => subdomain, :token => api_token, :room_id => room_id)
  25 +
  26 + # post the issue to the campfire room
  27 + campy.speak "[errbit] http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} #{notification_description problem}"
  28 + end
  29 +end
0 30 \ No newline at end of file
... ...
app/views/apps/_fields.html.haml
... ... @@ -46,4 +46,5 @@
46 46 = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy'
47 47  
48 48 = render "issue_tracker_fields", :f => f
  49 += render "service_notification_fields", :f => f
49 50  
... ...
app/views/apps/_service_notification_fields.html.haml 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +%fieldset
  2 + %legend Notification Service
  3 + = f.fields_for :notification_service do |w|
  4 + %div.notification_service.nested
  5 + %div.choose
  6 + = label_tag :type_none, :for => label_for_attr(w, 'type_notificationservice'), :class => "label_radio none" do
  7 + = w.radio_button :type, "NotificationService", 'data-section' => 'none'
  8 + (None)
  9 + - NotificationService.subclasses.each do |notification_service|
  10 + = label_tag "type_#{notification_service.label}:", :for => label_for_attr(w, "type_#{notification_service.name.downcase.gsub(':','')}"), :class => "label_radio #{notification_service.label}" do
  11 + = w.radio_button :type, notification_service.name, 'data-section' => notification_service.label
  12 + = notification_service.name[/::(.*)Service/,1].titleize
  13 +
  14 + %div.notification_params.none{:class => (w.object && !(w.object.class < NotificationService)) ? 'chosen' : nil}
  15 + - NotificationService.subclasses.each do |notification_service|
  16 + %div.notification_params{:class => (w.object.is_a?(notification_service) ? 'chosen ' : '') << notification_service.label}
  17 + - notification_service::Fields.each do |field, field_info|
  18 + = w.label field, field_info[:label] || field.to_s.titleize
  19 + - field_type = field == :password ? :password_field : :text_field
  20 + = w.send field_type, field, :placeholder => field_info[:placeholder], :value => w.object.send(field)
  21 +
  22 + .image_preloader
  23 + - (NotificationService.subclasses.map{|t| t.label } << 'none').each do |notification_service|
  24 + = image_tag "#{notification_service}_inactive.png"
  25 + = image_tag "#{notification_service}_create.png"
... ...
app/views/apps/index.html.haml
... ... @@ -8,6 +8,8 @@
8 8 %th Name
9 9 - if any_github_repos?
10 10 %th GitHub Repo
  11 + - if any_notification_services?
  12 + %th Notification Service
11 13 - if any_issue_trackers?
12 14 %th Tracker
13 15 - if any_deploys?
... ... @@ -21,6 +23,10 @@
21 23 %td.github_repo
22 24 - if app.github_repo?
23 25 = link_to(app.github_repo, app.github_url, :target => '_blank')
  26 + - if any_notification_services?
  27 + %td.notification_service
  28 + - if app.notification_service_configured?
  29 + = image_tag("#{app.notification_service.label}_goto.png")
24 30 - if any_issue_trackers?
25 31 %td.issue_tracker
26 32 - if app.issue_tracker_configured?
... ...
config/environments/production.rb
... ... @@ -59,5 +59,8 @@ Errbit::Application.configure do
59 59  
60 60 # Send deprecation notices to registered listeners
61 61 config.active_support.deprecation = :notify
  62 +
  63 + # enable HTTPS
  64 + config.middleware.insert_before Rack::Lock, "Rack::SSL"
62 65 end
63 66  
... ...
spec/fabricators/issue_tracker_fabricator.rb
... ... @@ -24,4 +24,3 @@ Fabricator :github_issues_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;Issu
24 24 project_id 'test_account/test_project'
25 25 username 'test_username'
26 26 end
27   -
... ...
spec/fabricators/notification_service_fabricator.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +Fabricator :notification_service do
  2 + app!
  3 + room_id { sequence :word }
  4 + api_token { sequence :word }
  5 + subdomain { sequence :word }
  6 +end
  7 +
  8 +%w(campfire).each do |t|
  9 + Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service"
  10 +end
... ...
spec/models/notice_observer_spec.rb
... ... @@ -25,7 +25,6 @@ describe NoticeObserver do
25 25 end
26 26  
27 27 describe "email notifications for a resolved issue" do
28   -
29 28 before do
30 29 Errbit::Config.per_app_email_at_notices = true
31 30 @app = Fabricate(:app_with_watcher, :email_at_notices => [1])
... ... @@ -43,4 +42,28 @@ describe NoticeObserver do
43 42 Fabricate(:notice, :err => @err)
44 43 end
45 44 end
  45 +
  46 + describe "should send a notification if a notification service is configured" do
  47 + let(:app) { app = Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service))}
  48 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) }
  49 +
  50 + before do
  51 + Errbit::Config.per_app_email_at_notices = true
  52 + end
  53 +
  54 + after do
  55 + Errbit::Config.per_app_email_at_notices = false
  56 + end
  57 +
  58 + it "should create a campfire notification" do
  59 + err.problem.stub(:notices_count) { 1 }
  60 + app.notification_service.stub!(:create_notification).and_return(true)
  61 + app.stub!(:notification_recipients => %w('ryan@system88.com'))
  62 + app.notification_service.should_receive(:create_notification)
  63 +
  64 + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},
  65 + :backtrace => [{ :error => 'Le Broken' }], :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' })
  66 + end
  67 + end
  68 +
46 69 end
... ...
spec/models/notification_service/campfire_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationService::CampfireService do
  4 + it "it should send a notification to campfire" do
  5 + # setup
  6 + notice = Fabricate :notice
  7 + notification_service = Fabricate :campfire_notification_service, :app => notice.app
  8 + problem = notice.problem
  9 +
  10 + #campy stubbing
  11 + campy = mock('CampfireService')
  12 + Campy::Room.stub(:new).and_return(campy)
  13 + campy.stub(:speak) { true }
  14 +
  15 + #assert
  16 + campy.should_receive(:speak)
  17 +
  18 + notification_service.create_notification(problem)
  19 + end
  20 +end
  21 +
... ...