caffeinate 0.1.4 → 0.5.0

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 (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