caffeinate 0.1.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +58 -43
  3. data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +17 -2
  4. data/app/models/caffeinate/campaign.rb +40 -1
  5. data/app/models/caffeinate/campaign_subscription.rb +52 -11
  6. data/app/models/caffeinate/mailing.rb +26 -3
  7. data/app/views/caffeinate/campaign_subscriptions/subscribe.html.erb +3 -0
  8. data/app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb +3 -0
  9. data/app/views/layouts/_caffeinate.html.erb +11 -0
  10. data/config/locales/en.yml +6 -0
  11. data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +1 -0
  12. data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +3 -0
  13. data/db/migrate/20201124183419_create_caffeinate_mailings.rb +4 -1
  14. data/lib/caffeinate.rb +2 -0
  15. data/lib/caffeinate/action_mailer.rb +4 -4
  16. data/lib/caffeinate/action_mailer/extension.rb +17 -1
  17. data/lib/caffeinate/action_mailer/interceptor.rb +4 -2
  18. data/lib/caffeinate/action_mailer/observer.rb +5 -4
  19. data/lib/caffeinate/active_record/extension.rb +3 -2
  20. data/lib/caffeinate/configuration.rb +4 -1
  21. data/lib/caffeinate/drip.rb +22 -35
  22. data/lib/caffeinate/drip_evaluator.rb +35 -0
  23. data/lib/caffeinate/dripper/base.rb +13 -14
  24. data/lib/caffeinate/dripper/batching.rb +22 -0
  25. data/lib/caffeinate/dripper/callbacks.rb +74 -6
  26. data/lib/caffeinate/dripper/campaign.rb +8 -9
  27. data/lib/caffeinate/dripper/defaults.rb +4 -2
  28. data/lib/caffeinate/dripper/delivery.rb +8 -8
  29. data/lib/caffeinate/dripper/drip.rb +46 -16
  30. data/lib/caffeinate/dripper/inferences.rb +29 -0
  31. data/lib/caffeinate/dripper/perform.rb +16 -5
  32. data/lib/caffeinate/dripper/periodical.rb +24 -0
  33. data/lib/caffeinate/engine.rb +12 -9
  34. data/lib/caffeinate/helpers.rb +24 -0
  35. data/lib/caffeinate/mail_ext.rb +12 -0
  36. data/lib/caffeinate/url_helpers.rb +10 -0
  37. data/lib/caffeinate/version.rb +1 -1
  38. data/lib/generators/caffeinate/install_generator.rb +5 -1
  39. data/lib/generators/caffeinate/templates/caffeinate.rb +11 -1
  40. metadata +13 -5
  41. data/app/views/layouts/caffeinate/application.html.erb +0 -15
  42. data/app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb +0 -1
  43. data/lib/caffeinate/action_mailer/helpers.rb +0 -12
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Campaign methods for `Caffeinate::Dripper`.
5
6
  module Campaign
6
7
  # :nodoc:
7
8
  def self.included(klass)
@@ -10,7 +11,7 @@ module Caffeinate
10
11
 
11
12
  # The campaign for this Dripper
12
13
  #
13
- # @return Caffeinate::Campaign
14
+ # @return `Caffeinate::Campaign`
14
15
  def campaign
15
16
  self.class.caffeinate_campaign
16
17
  end
@@ -18,7 +19,7 @@ module Caffeinate
18
19
  module ClassMethods
19
20
  # Sets the campaign on the Dripper and resets any existing `@caffeinate_campaign`
20
21
  #
21
- # class OrdersDripper
22
+ # class OrdersDripper < ApplicationDripper
22
23
  # campaign :order_drip
23
24
  # end
24
25
  #
@@ -27,25 +28,23 @@ module Caffeinate
27
28
  # self.name.delete_suffix("Campaign").underscore
28
29
  #
29
30
  # @param [Symbol] slug The slug of a persisted `Caffeinate::Campaign`.
30
- def campaign(slug)
31
+ def campaign=(slug)
31
32
  @caffeinate_campaign = nil
32
33
  @_campaign_slug = slug.to_sym
33
34
  Caffeinate.register_dripper(@_campaign_slug, name)
34
35
  end
35
36
 
36
- # Returns the `Caffeinate::Campaign` object for the Campaign
37
+ # Returns the `Caffeinate::Campaign` object for the Dripper
37
38
  def caffeinate_campaign
38
39
  return @caffeinate_campaign if @caffeinate_campaign.present?
39
40
 
40
- @caffeinate_campaign = ::Caffeinate::Campaign.find_by(slug: campaign_slug)
41
- return @caffeinate_campaign if @caffeinate_campaign
42
-
43
- raise(::ActiveRecord::RecordNotFound, "Unable to find ::Caffeinate::Campaign with slug #{campaign_slug}.")
41
+ @caffeinate_campaign = ::Caffeinate::Campaign[campaign_slug]
44
42
  end
43
+ alias campaign caffeinate_campaign
45
44
 
46
45
  # The defined slug or the inferred slug
47
46
  def campaign_slug
48
- @_campaign_slug || name.delete_suffix('Campaign')
47
+ @_campaign_slug || inferred_campaign_slug
49
48
  end
50
49
  end
51
50
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Handles the default DSL for a `Caffeinate::Dripper`.
5
6
  module Defaults
6
7
  # :nodoc:
7
8
  def self.included(klass)
@@ -11,7 +12,7 @@ module Caffeinate
11
12
  module ClassMethods
12
13
  # The defaults set in the Campaign
13
14
  def defaults
14
- @defaults ||= { mailer_class: inferred_mailer_class }
15
+ @defaults ||= { mailer_class: inferred_mailer_class, batch_size: ::Caffeinate.config.batch_size }
15
16
  end
16
17
 
17
18
  # The default options for the Campaign
@@ -23,7 +24,8 @@ module Caffeinate
23
24
  # @param [Hash] options The options to set defaults with
24
25
  # @option options [String] :mailer_class The mailer class
25
26
  def default(options = {})
26
- options.assert_valid_keys(:mailer_class, :mailer, :using)
27
+ options.symbolize_keys!
28
+ options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
27
29
  @defaults = options
28
30
  end
29
31
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # Handles delivery of a Caffeinate::Mailer for a Caffeinate::Dripper
5
+ # Handles delivery of a `Caffeinate::Mailer` for a `Caffeinate::Dripper`.
6
6
  module Delivery
7
7
  # :nodoc:
8
8
  def self.included(klass)
@@ -14,13 +14,13 @@ module Caffeinate
14
14
  #
15
15
  # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
16
  def deliver!(mailing)
17
- Thread.current[:current_caffeinate_mailing] = mailing
18
-
19
- if mailing.drip.parameterized?
20
- mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action).deliver
21
- else
22
- mailing.mailer_class.constantize.send(mailing.mailer_action, mailing).deliver
23
- end
17
+ message = if mailing.drip.parameterized?
18
+ mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action)
19
+ else
20
+ mailing.mailer_class.constantize.send(mailing.mailer_action, mailing)
21
+ end
22
+ message.caffeinate_mailing = mailing
23
+ message.deliver
24
24
  end
25
25
  end
26
26
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # The Drip DSL for registering a drip
5
+ # The Drip DSL for registering a drip.
6
6
  module Drip
7
7
  # A collection of Drip objects for a `Caffeinate::Dripper`
8
8
  class DripCollection
@@ -10,21 +10,55 @@ module Caffeinate
10
10
 
11
11
  def initialize(dripper)
12
12
  @dripper = dripper
13
- @drips = []
13
+ @drips = {}
14
+ end
15
+
16
+ def for(action)
17
+ @drips[action.to_sym]
14
18
  end
15
19
 
16
20
  # Register the drip
17
21
  def register(action, options, &block)
18
- @drips << ::Caffeinate::Drip.new(@dripper, action, options, &block)
22
+ options = validate_drip_options(action, options)
23
+
24
+ @drips[action.to_sym] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
19
25
  end
20
26
 
21
27
  def each(&block)
22
- @drips.each { |drip| block.call(drip) }
28
+ @drips.each { |action_name, drip| block.call(action_name, drip) }
29
+ end
30
+
31
+ def values
32
+ @drips.values
23
33
  end
24
34
 
25
35
  def size
26
36
  @drips.size
27
37
  end
38
+
39
+ def [](val)
40
+ @drips[val]
41
+ end
42
+
43
+ private
44
+
45
+ def validate_drip_options(action, options)
46
+ options.symbolize_keys!
47
+ options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer)
48
+ options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
49
+ options[:using] ||= @dripper.defaults[:using]
50
+ options[:step] ||= @dripper.drips.size + 1
51
+
52
+ if options[:mailer_class].nil?
53
+ raise ArgumentError, "You must define :mailer_class or :mailer in the options for #{action.inspect} on #{@dripper.inspect}"
54
+ end
55
+
56
+ if options[:every].nil? && options[:delay].nil?
57
+ raise ArgumentError, "You must define :delay in the options for #{action.inspect} on #{@dripper.inspect}"
58
+ end
59
+
60
+ options
61
+ end
28
62
  end
29
63
 
30
64
  # :nodoc:
@@ -33,9 +67,14 @@ module Caffeinate
33
67
  end
34
68
 
35
69
  module ClassMethods
70
+ # A collection of Drip objects associated with a given `Caffeinate::Dripper`
71
+ def drip_collection
72
+ @drip_collection ||= DripCollection.new(self)
73
+ end
74
+
36
75
  # A collection of Drip objects associated with a given `Caffeinate::Dripper`
37
76
  def drips
38
- @drips ||= DripCollection.new(self)
77
+ drip_collection.values
39
78
  end
40
79
 
41
80
  # Register a drip on the Dripper
@@ -47,18 +86,9 @@ module Caffeinate
47
86
  # @option options [String] :mailer_class The mailer_class
48
87
  # @option options [Integer] :step The order in which the drip is executed
49
88
  # @option options [ActiveSupport::Duration] :delay When the drip should be ran
89
+ # @option options [Symbol] :using set to :parameters if the mailer action uses ActionMailer::Parameters
50
90
  def drip(action_name, options = {}, &block)
51
- options.assert_valid_keys(:mailer_class, :step, :delay, :using, :mailer)
52
- options[:mailer_class] ||= options[:mailer] || defaults[:mailer_class]
53
- options[:using] ||= defaults[:using]
54
- options[:step] ||= drips.size + 1
55
-
56
- if options[:mailer_class].nil?
57
- raise ArgumentError, "You must define :mailer_class or :mailer in the options for :#{action_name}"
58
- end
59
- raise ArgumentError, "You must define :delay in the options for :#{action_name}" if options[:delay].nil?
60
-
61
- drips.register(action_name, options, &block)
91
+ drip_collection.register(action_name, options, &block)
62
92
  end
63
93
  end
64
94
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Includes the inferred methods based on a Dripper name.
6
+ module Inferences
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # The inferred mailer class
13
+ def inferred_mailer_class
14
+ klass_name = "#{name.delete_suffix('Dripper')}Mailer"
15
+ klass = klass_name.safe_constantize
16
+ return nil unless klass
17
+ return klass_name if klass < ::ActionMailer::Base
18
+
19
+ nil
20
+ end
21
+
22
+ # The inferred mailer class
23
+ def inferred_campaign_slug
24
+ name.delete_suffix('Dripper').to_s.underscore
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,23 +2,34 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`
5
+ # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`.
6
6
  module Perform
7
7
  # :nodoc:
8
8
  def self.included(klass)
9
9
  klass.extend ClassMethods
10
10
  end
11
11
 
12
- # Delivers the next_caffeinate_mailer for the campaign's subscribers.
12
+ # Delivers the next_caffeinate_mailer for the campaign's subscribers. Handles with batches based on `batch_size`.
13
13
  #
14
14
  # OrderDripper.new.perform!
15
15
  #
16
16
  # @return nil
17
17
  def perform!
18
- campaign.caffeinate_campaign_subscriptions.joins(:next_caffeinate_mailing).includes(:next_caffeinate_mailing).each do |subscriber|
19
- subscriber.next_caffeinate_mailing.process!
18
+ run_callbacks(:before_perform, self)
19
+ Caffeinate::Mailing
20
+ .upcoming
21
+ .unsent
22
+ .joins(:caffeinate_campaign_subscription)
23
+ .merge(Caffeinate::CampaignSubscription.active)
24
+ .in_batches(of: self.class.batch_size)
25
+ .each do |batch|
26
+ run_callbacks(:on_perform, self, batch)
27
+ batch.each do |mailing|
28
+ mailing.process!
29
+ end
20
30
  end
21
- true
31
+ run_callbacks(:after_perform, self)
32
+ nil
22
33
  end
23
34
 
24
35
  module ClassMethods
@@ -0,0 +1,24 @@
1
+ module Caffeinate
2
+ module Dripper
3
+ module Periodical
4
+ def self.included(klass)
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def periodical(action_name, every:, start: -> { ::Caffeinate.config.time_now }, **options, &block)
10
+ options[:start] = start
11
+ options[:every] = every
12
+ drip(action_name, options, &block)
13
+ after_send do |mailing, _message|
14
+ if mailing.drip.action == action_name
15
+ next_mailing = mailing.dup
16
+ next_mailing.send_at = mailing.drip.send_at(mailing)
17
+ next_mailing.save!
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,29 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'caffeinate/helpers'
3
4
  require 'caffeinate/action_mailer'
4
5
  require 'caffeinate/active_record/extension'
5
6
 
6
7
  module Caffeinate
7
- # :nodoc:
8
+ # Adds Caffeinate to Rails
8
9
  class Engine < ::Rails::Engine
9
10
  isolate_namespace Caffeinate
10
11
 
12
+ config.to_prepare do
13
+ Dir.glob(Rails.root.join(Caffeinate.config.drippers_path, "**", "*.rb")).each do |dripper|
14
+ require dripper
15
+ end
16
+ end
17
+
11
18
  ActiveSupport.on_load(:action_mailer) do
12
19
  include ::Caffeinate::ActionMailer::Extension
13
20
  ::ActionMailer::Base.register_interceptor(::Caffeinate::ActionMailer::Interceptor)
14
21
  ::ActionMailer::Base.register_observer(::Caffeinate::ActionMailer::Observer)
15
22
  end
16
23
 
17
- unless config.eager_load
18
- config.to_prepare do
19
- Dir.glob("#{Rails.root}/app/drippers/**/*.rb").each do |f|
20
- require f
21
- end
22
- end
23
- end
24
-
25
24
  ActiveSupport.on_load(:active_record) do
26
25
  extend ::Caffeinate::ActiveRecord::Extension
27
26
  end
27
+
28
+ ActiveSupport.on_load(:action_view) do
29
+ ActionView::Base.include ::Caffeinate::Helpers
30
+ end
28
31
  end
29
32
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ # URL helpers for accessing the mounted Caffeinate instance.
5
+ module Helpers
6
+ def caffeinate_unsubscribe_url(subscription, **options)
7
+ opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
8
+ Caffeinate::Engine.routes.url_helpers.unsubscribe_campaign_subscription_url(token: subscription.token, **opts)
9
+ end
10
+
11
+ def caffeinate_subscribe_url(subscription, **options)
12
+ opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
13
+ Caffeinate::Engine.routes.url_helpers.subscribe_campaign_subscription_url(token: subscription.token, **opts)
14
+ end
15
+
16
+ def caffeinate_unsubscribe_path(subscription, **options)
17
+ Caffeinate::Engine.routes.url_helpers.unsubscribe_campaign_subscription_path(token: subscription.token, **options)
18
+ end
19
+
20
+ def caffeinate_subscribe_path(subscription, **options)
21
+ Caffeinate::Engine.routes.url_helpers.subscribe_campaign_subscription_path(token: subscription.token, **options)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ # Extend Mail::Message to account for a Caffeinate::Mailing
5
+ class Message
6
+ attr_accessor :caffeinate_mailing
7
+
8
+ def caffeinate?
9
+ caffeinate_mailing.present?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'caffeinate/helpers'
4
+
5
+ module Caffeinate
6
+ # Convenience class for the URL helpers.
7
+ class UrlHelpers
8
+ extend Helpers
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.1.4'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Generators
5
- # :nodoc:
5
+ # Installs Caffeinate
6
6
  class InstallGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path('templates', __dir__)
8
8
  include ::Rails::Generators::Migration
@@ -19,6 +19,10 @@ module Caffeinate
19
19
  template 'application_dripper.rb', 'app/drippers/application_dripper.rb'
20
20
  end
21
21
 
22
+ def install_routes
23
+ inject_into_file 'config/routes.rb', "\n mount ::Caffeinate::Engine => '/caffeinate'", after: /Rails.application.routes.draw do/
24
+ end
25
+
22
26
  # :nodoc:
23
27
  def self.next_migration_number(_path)
24
28
  if @prev_migration_nr
@@ -6,7 +6,7 @@ Caffeinate.setup do |config|
6
6
  # Used for when we set a datetime column to "now" in the database
7
7
  #
8
8
  # Default:
9
- # -> { Time.current }
9
+ # config.now = -> { Time.current }
10
10
  #
11
11
  # config.now = -> { DateTime.now }
12
12
  #
@@ -21,4 +21,14 @@ Caffeinate.setup do |config|
21
21
  #
22
22
  # config.async_delivery = true
23
23
  # config.mailing_job = 'MyCustomCaffeinateJob'
24
+ #
25
+ # == Batching
26
+ #
27
+ # When a Dripper is performed and sends the mails, we use `find_in_batches`. Use `batch_size` to set the batch size.
28
+ # You can set this on a dripper as well for more granular control.
29
+ #
30
+ # Default:
31
+ # config.batch_size = 1_000
32
+ #
33
+ # config.batch_size = 100
24
34
  end