acts_as_notifier 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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