acts_as_notifier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +186 -0
  4. data/Rakefile +36 -0
  5. data/lib/acts_as_notifier.rb +185 -0
  6. data/lib/acts_as_notifier/config.rb +5 -0
  7. data/lib/acts_as_notifier/version.rb +3 -0
  8. data/test/acts_as_notifier/acts_as_notifier_test.rb +44 -0
  9. data/test/acts_as_notifier/conditions_test.rb +42 -0
  10. data/test/acts_as_notifier/config_test.rb +37 -0
  11. data/test/acts_as_notifier/mailer_test.rb +46 -0
  12. data/test/acts_as_notifier/recipients_test.rb +51 -0
  13. data/test/dummy/README.rdoc +261 -0
  14. data/test/dummy/Rakefile +7 -0
  15. data/test/dummy/app/assets/javascripts/application.js +15 -0
  16. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  17. data/test/dummy/app/controllers/application_controller.rb +3 -0
  18. data/test/dummy/app/helpers/application_helper.rb +2 -0
  19. data/test/dummy/app/mailers/my_mailer.rb +27 -0
  20. data/test/dummy/app/models/widget.rb +65 -0
  21. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  22. data/test/dummy/config.ru +4 -0
  23. data/test/dummy/config/application.rb +59 -0
  24. data/test/dummy/config/boot.rb +10 -0
  25. data/test/dummy/config/database.yml +25 -0
  26. data/test/dummy/config/environment.rb +5 -0
  27. data/test/dummy/config/environments/development.rb +37 -0
  28. data/test/dummy/config/environments/production.rb +67 -0
  29. data/test/dummy/config/environments/test.rb +37 -0
  30. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  31. data/test/dummy/config/initializers/inflections.rb +15 -0
  32. data/test/dummy/config/initializers/mime_types.rb +5 -0
  33. data/test/dummy/config/initializers/secret_token.rb +7 -0
  34. data/test/dummy/config/initializers/session_store.rb +8 -0
  35. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  36. data/test/dummy/config/locales/en.yml +5 -0
  37. data/test/dummy/config/routes.rb +58 -0
  38. data/test/dummy/db/migrate/20131229201627_create_widgets.rb +9 -0
  39. data/test/dummy/db/schema.rb +22 -0
  40. data/test/dummy/db/test.sqlite3 +0 -0
  41. data/test/dummy/log/development.log +21 -0
  42. data/test/dummy/log/test.log +41137 -0
  43. data/test/dummy/public/404.html +26 -0
  44. data/test/dummy/public/422.html +26 -0
  45. data/test/dummy/public/500.html +25 -0
  46. data/test/dummy/public/favicon.ico +0 -0
  47. data/test/dummy/script/rails +6 -0
  48. data/test/test_helper.rb +17 -0
  49. metadata +232 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b54dedfd2daec809d63e1158f2f8a6c0f7a3e1d1
4
+ data.tar.gz: 008889b4a39b1d0ce4db7bfe072c4d35f22f1031
5
+ SHA512:
6
+ metadata.gz: 59b79b96dd4b6656d0efcec8bc4d5ceb165fd863b5eb45bcfc2d3e81c7245b00ab9f5e446db2ceff712c282682063720ede9f10e8061dcff797baab6d427e756
7
+ data.tar.gz: a482837b9a95c9778193e998dde01e867608529269e21844bbdb406c1fcfeaf046759ba29222d5d7a9000925b7c7742c2f8874e00871c0b2cd79e2132bfe7997
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Michael Johnson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # acts_as_notifier
2
+
3
+ Easily define ActiveRecord callbacks that notify users of changes via email. Acts_as_notifier dries up your code with
4
+ a simple, readable DSL, the ability to configure defaults, and by using delayed_job automatically if enabled. The DSL
5
+ looks especially good when you have multiple notifications defined in a model.
6
+
7
+ Instead of this:
8
+
9
+ ```ruby
10
+ class MyModel < ActiveRecord::Base
11
+ def after_create
12
+ if alerts_enabled?
13
+ if MyMailer.respond_to?(:delay)
14
+ MyMailer.delay.new_widget_alert(get_recipients, self)
15
+ else
16
+ MyMailer.new_widget_alert(get_recipients, self).deliver
17
+ end
18
+ end
19
+ end
20
+ end
21
+ ```
22
+
23
+ With acts_as_notifier you write this:
24
+
25
+ ```ruby
26
+ # in config/initializers/acts_as_notifier.rb
27
+ ActsAsNotifier::Config.default_mailer = :MyMailer
28
+ ActsAsNotifier::Config.default_mailer = :new_widget_alert
29
+
30
+ class MyModel < ActiveRecord::Base
31
+ acts_as_notifier do
32
+ after_create do
33
+ notify :get_recipients, :if => :alerts_enabled?
34
+ end
35
+ end
36
+ end
37
+ ```
38
+
39
+ ## When Should I Use This?
40
+
41
+ Honestly, using an ActiveRecord callback to trigger email notifications is an [anti-pattern](#soapbox).
42
+ Acts_as_notifier is a good solution when you have an existing Rails app, possibly with overly-complicated controllers,
43
+ and refactoring is not feasible. In that case, acts_as_notifier gets you up and running quickly and hides some of the
44
+ added complexity.
45
+
46
+ ## Installation
47
+
48
+ Add this line to your application's Gemfile:
49
+
50
+ ```ruby
51
+ gem 'acts_as_notifier'
52
+ ```
53
+
54
+ And then execute:
55
+
56
+ ```ruby
57
+ $ bundle
58
+ ```
59
+
60
+ Or install it yourself as:
61
+
62
+ ```ruby
63
+ $ gem install acts_as_notifier
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ```ruby
69
+ # in config/initializers/acts_as_notifier.rb
70
+ ActsAsNotifier::Config.default_mailer = :MyMailer
71
+ ActsAsNotifier::Config.default_mailer = :change_notification
72
+
73
+ # in app/models/my_model.rb
74
+ class MyModel < ActiveRecord::Base
75
+ acts_as_notifier do
76
+ after_create do
77
+ notify recipients, options
78
+ end
79
+ after_update do
80
+ notify recipients, options
81
+ end
82
+ after_save do
83
+ notify recipients, options
84
+ end
85
+ end
86
+ end
87
+
88
+ # in app/mailers/my_mailer.rb
89
+ class MyMailer < ActionMailer::Base
90
+ default from: 'test@example.com'
91
+
92
+ def change_notification(recipients, model)
93
+ mail(to: recipients, subject: "A #{model.class} with ID #{model.id} was changed")
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### recipients
99
+
100
+ Can be a string containing email addresses, any object with an 'email' method, an array of strings/objects,
101
+ or a proc or symbolic method name returning any of the above. Procs will be passed the options hash as a parameter.
102
+ Note that to make sure you get the up-to-date recipients at runtime, you will normally use a proc.
103
+
104
+ Examples:
105
+
106
+ ```ruby
107
+ notify 'bob@somewhere.com', options
108
+ notify :find_admin_users, options
109
+
110
+ # note use of a proc to lazy-evaluate the ActiveRecord query
111
+ notify Proc.new { User.where(:should_notify, true) }, options
112
+
113
+ # pass yourself custom options in addition to the acts_as_notifiers options
114
+ notify :get_recipients, :role => :admins
115
+ def self.get_recipients(options)
116
+ User.find_all_by_role(options[:role])
117
+ end
118
+ ```
119
+
120
+ ### options
121
+
122
+ <table>
123
+ <tr><td>:if</td><td>A string, proc, or name of a method that must evaluate to a truthy value in order for the notification to be sent. Procs and strings are evaluated in the context of the model instance. Procs are be passed the options hash as a parameter.</td></tr>
124
+ <tr><td>:mailer</td><td>A class inheriting from ActionMailer::Base, may be a string or symbol or actual class.</td></tr>
125
+ <tr><td>:method</td><td>Mailer method to invoke. Should accept recipients string and the ActiveRecord model as params.</td></tr>
126
+ <tr><td>custom</td><td>Any additional options will be saved and passed to :if and recipients procs.</td></tr>
127
+ </table>
128
+
129
+ Examples:
130
+
131
+
132
+ ```ruby
133
+ acts_as_notifier do
134
+ after_create do
135
+ # different ways of specifying :if condition
136
+ notify :recipients, :if => 'alerts_enabled?'
137
+ notify :recipients, :if => :alerts_enabled?
138
+ notify :recipients, :if => proc { alerts_enabled? }
139
+
140
+ # passing your own custom option to an if condition proc
141
+ notify :recipients, :if => proc {|opts| alerts_enabled_for_role?(opts[:role]) }, :role => :admin
142
+
143
+ # specifying a mailer
144
+ notify :recipients, :mailer => 'MyMailer'
145
+ notify :recipients, :mailer => :MyMailer
146
+ notify :recipients, :mailer => MyMailer
147
+
148
+ # specifying mailer method (in MyMailer class, def new_record_notification(recipients, model))
149
+ notify :recipients, :mailer => MyMailer, :method => :new_record_notification
150
+
151
+ # complete example
152
+ notify proc { User.where(wants_alerts: true) }, :if => :alerts_enabled?, :mailer => MyMailer, :method => :new_widget_alert
153
+ end
154
+ x
155
+ # another complete example
156
+ after_save do
157
+ notify :owner, :if => ->(widget){ widget.broken? }, :mailer => MyMailer, :method => :broken_widget_alert
158
+ end
159
+ end
160
+ ```
161
+
162
+ ## Configuration
163
+
164
+ <table>
165
+ <tr><td>ActsAsNotifier::Config.use_delayed_job=</td><td>true/false</td></tr>
166
+ <tr><td>ActsAsNotifier::Config.default_mailer=</td><td>class inheriting from ActionMailer::Base</td></tr>
167
+ <tr><td>ActsAsNotifier::Config.default_method=</td><td>mailer method to invoke, takes recipient string and ActiveRecord model as params</td></tr>
168
+ <tr><td>ActsAsNotifier::Config.disabled=</td><td>true/false, can be globally enabled or disabled at any time</td></tr>
169
+ </table>
170
+
171
+ ## Contributing
172
+
173
+ 1. Fork it
174
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
175
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
176
+ 4. Push to the branch (`git push origin my-new-feature`)
177
+ 5. Create new Pull Request
178
+
179
+ ## <a name="soapbox"></a>Soapbox
180
+
181
+ Why do I consider using ActiveRecord callbacks to trigger email notifications an anti-pattern? Callback-based behaviors
182
+ are difficult to disable or isolate in testing and can result in mysterious, difficult-to-find bugs. In this particular
183
+ case, using them violates the single responsibility principle in that a model should only be concerned with persisting
184
+ your data. It also creates undesirable connections between your model, other models, and mailer classes. A better
185
+ solution is to build business logic classes that handle creating and updating models, sending notifications, and are
186
+ easier to isolate for testing.
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'ActsAsNotifier'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ Bundler::GemHelper.install_tasks
24
+
25
+ require 'rake/testtask'
26
+
27
+ Rake::TestTask.new(:test) do |t|
28
+ t.libs << 'lib'
29
+ t.libs << 'test'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = false
32
+ end
33
+
34
+ task :default => :test
35
+
36
+ require "bundler/gem_tasks"
@@ -0,0 +1,185 @@
1
+ require "acts_as_notifier/version"
2
+ require "acts_as_notifier/config"
3
+
4
+ # Allows an ActiveRecord model to tie notification emails to create and save callbacks with conditions
5
+ #
6
+ # Usage:
7
+ # acts_as_notifier do
8
+ # after_create/save/update do
9
+ # notify recipients, options
10
+ # end
11
+ # end
12
+ #
13
+ # Params:
14
+ # recipients - can be a string containing email addresses, any object with an 'email' method, an array of
15
+ # strings/objects, or a proc or symbolic method name returning any of the above. procs will be
16
+ # passed the options hash as a parameter.
17
+ #
18
+ # options
19
+ # :if - string to eval, proc, or method name, !!result must equal true for notification to be sent. procs will
20
+ # be passed the options hash as a parameter.
21
+ # :mailer - Typically a class inheriting from ActionMailer::Base.
22
+ # May be any class or object with a method accepting a recipients string and instance of model that triggered the notification.
23
+ # May also be a string or symbol that evals to a class or object.
24
+ # :method - mailer class method to invoke, should accept recipient list and sending ActiveRecord model as params
25
+ # custom - any additional options can be saved in the action and will be available to procs
26
+ #
27
+ # Examples:
28
+ # acts_as_notifier do
29
+ # after_create do
30
+ # notify proc { User.where(wants_alerts: true) }, :if => proc { send_alert? }, :mailer => MyMailer, :method => :new_widget_alert
31
+ # end
32
+ # after_save do
33
+ # notify :owner, :if => ->widget{ widget.broken? }, :mailer => MyMailer, :method => :broken_widget_alert
34
+ # end
35
+ # end
36
+ #
37
+ # Configuration:
38
+ # ActsAsNotifier::Config.use_delayed_job = true/false
39
+ # ActsAsNotifier::Config.default_mailer = a class inheriting from ActionMailer::Base
40
+ # ActsAsNotifier::Config.default_method = mailer class method to invoke, should accept recipient list and sending
41
+ # ActiveRecord model as params
42
+ # ActsAsNotifier::Config.disabled = true/false, can be globally enabled or disabled at any time
43
+
44
+ module ActsAsNotifier
45
+ extend ActiveSupport::Concern
46
+
47
+ module ClassMethods
48
+ def acts_as_notifier(&block)
49
+ cattr_accessor :notifier_actions
50
+ self.notifier_actions = NotificationDsl.evaluate(&block).presence || []
51
+ include LocalInstanceMethods
52
+ end
53
+ end
54
+
55
+ module LocalInstanceMethods
56
+ extend ActiveSupport::Concern
57
+
58
+ included do
59
+ after_create :notifier_create_handler
60
+ after_save :notifier_save_handler
61
+ after_update :notifier_update_handler
62
+ end
63
+
64
+ # find a notification action matching options and trigger it, ignoring conditions
65
+ def force_notification(options = {})
66
+ opts = options.to_a
67
+ action = self.class.notifier_actions.find { |action| action.to_a & opts == opts }
68
+ notifier_send(action)
69
+ end
70
+
71
+ private
72
+
73
+ def notifier_create_handler
74
+ notifier_handler(:after_create) unless ActsAsNotifier::Config.disabled
75
+ end
76
+
77
+ def notifier_save_handler
78
+ notifier_handler(:after_save) unless ActsAsNotifier::Config.disabled
79
+ end
80
+
81
+ def notifier_update_handler
82
+ notifier_handler(:after_update) unless ActsAsNotifier::Config.disabled
83
+ end
84
+
85
+ def notifier_handler(callback_type)
86
+ self.class.notifier_actions.each do |action|
87
+ notifier_send(action) if action[:callback_type].to_sym == callback_type && notifier_conditions_satisfied?(action)
88
+ end
89
+ end
90
+
91
+ def notifier_conditions_satisfied?(action)
92
+ condition = action[:if]
93
+ condition = case condition
94
+ when String
95
+ instance_eval(condition)
96
+ when Proc
97
+ instance_exec(action, &condition)
98
+ when Symbol
99
+ self.send(condition)
100
+ when nil
101
+ true
102
+ else
103
+ condition
104
+ end
105
+ !!condition
106
+ end
107
+
108
+ def notifier_recipients(action)
109
+ recipients = action[:recipients]
110
+ recipients = case recipients
111
+ when Proc
112
+ instance_exec(action, &recipients)
113
+ when Symbol
114
+ self.send(recipients)
115
+ else
116
+ recipients
117
+ end
118
+ recipients = [ recipients ] unless recipients.is_a?(Array)
119
+ recipient_list = recipients.map {|r| r.respond_to?(:email) ? r.email : r }.join(', ')
120
+ recipient_list
121
+ end
122
+
123
+ def notifier_send(action)
124
+ if (recipients = notifier_recipients(action)).present?
125
+ mailer, method = notifier_get_mailer(action)
126
+ if ActsAsNotifier::Config.use_delayed_job && mailer.respond_to?(:delay)
127
+ mailer.delay.send(method, recipients, self)
128
+ else
129
+ message = mailer.send(method, recipients, self)
130
+ message.deliver if message.respond_to?(:deliver)
131
+ end
132
+ end
133
+ end
134
+
135
+ def notifier_get_mailer(action)
136
+ mailer = action[:mailer] || ActsAsNotifier::Config.default_mailer
137
+ mailer = eval(mailer.to_s) if mailer.is_a?(String) || mailer.is_a?(Symbol)
138
+ raise "ActsAsNotifier invalid mailer configuration, mailer not specified" unless mailer.present?
139
+
140
+ method = action[:method] || ActsAsNotifier::Config.default_method
141
+ raise "ActsAsNotifier invalid mailer method configuration, method not specified" unless method.present?
142
+ raise "ActsAsNotifier invalid mailer method configuration, method must be a string or symbol" unless method.is_a?(String) || method.is_a?(Symbol)
143
+ raise "ActsAsNotifier invalid mailer method configuration, #{method} is not a valid method of #{mailer.to_s}" unless mailer.respond_to?(method)
144
+ return mailer, method
145
+ end
146
+ end
147
+
148
+ class NotificationDsl
149
+ attr_accessor :notification_actions
150
+
151
+ class << self
152
+ def evaluate(&script)
153
+ self.new.tap {|inst| inst.instance_eval(&script)}.notification_actions
154
+ end
155
+ end
156
+
157
+ def initialize
158
+ @current_callback_type = nil
159
+ @notification_actions = []
160
+ end
161
+
162
+ def after_create(&block)
163
+ @current_callback_type = :after_create
164
+ instance_eval(&block)
165
+ end
166
+
167
+ def after_save(&block)
168
+ @current_callback_type = :after_save
169
+ instance_eval(&block)
170
+ end
171
+
172
+ def after_update(&block)
173
+ @current_callback_type = :after_update
174
+ instance_eval(&block)
175
+ end
176
+
177
+ def notify(recipients, options = {})
178
+ action = { recipients: recipients, callback_type: @current_callback_type }.merge(options || {})
179
+ @notification_actions << action
180
+ end
181
+ end
182
+
183
+ end
184
+
185
+ ActiveRecord::Base.send(:include, ActsAsNotifier)
@@ -0,0 +1,5 @@
1
+ module ActsAsNotifier
2
+ module Config
3
+ mattr_accessor :use_delayed_job, :default_mailer, :default_method, :disabled
4
+ end
5
+ end