caffeinate 0.1.2 → 0.4.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 (44) 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 +34 -10
  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 +4 -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 +57 -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 -15
  30. data/lib/caffeinate/dripper/inferences.rb +29 -0
  31. data/lib/caffeinate/dripper/perform.rb +24 -5
  32. data/lib/caffeinate/dripper/periodical.rb +24 -0
  33. data/lib/caffeinate/dripper/subscriber.rb +2 -2
  34. data/lib/caffeinate/engine.rb +13 -1
  35. data/lib/caffeinate/helpers.rb +24 -0
  36. data/lib/caffeinate/mail_ext.rb +12 -0
  37. data/lib/caffeinate/url_helpers.rb +10 -0
  38. data/lib/caffeinate/version.rb +1 -1
  39. data/lib/generators/caffeinate/install_generator.rb +5 -1
  40. data/lib/generators/caffeinate/templates/caffeinate.rb +11 -1
  41. metadata +13 -5
  42. data/app/views/layouts/caffeinate/application.html.erb +0 -15
  43. data/app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb +0 -1
  44. data/lib/caffeinate/action_mailer/helpers.rb +0 -12
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/all'
4
+ require 'caffeinate/mail_ext'
4
5
  require 'caffeinate/engine'
5
6
  require 'caffeinate/drip'
7
+ require 'caffeinate/url_helpers'
6
8
  require 'caffeinate/configuration'
7
9
  require 'caffeinate/dripper/base'
8
10
  require 'caffeinate/deliver_async'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/action_mailer/extension'
4
- require 'caffeinate/action_mailer/helpers'
5
- require 'caffeinate/action_mailer/interceptor'
6
- require 'caffeinate/action_mailer/observer'
3
+ require 'mail'
4
+
5
+ # Includes all files in `caffeinate/action_mailer`
6
+ Dir["#{__dir__}/action_mailer/*"].each { |path| require "caffeinate/action_mailer/#{File.basename(path)}" }
@@ -2,11 +2,27 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
+ # Convenience for setting `@mailing`, and convenience methods for inferred `caffeinate_unsubscribe_url` and
6
+ # `caffeinate_subscribe_url`.
5
7
  module Extension
6
8
  def self.included(klass)
7
9
  klass.before_action do
8
- @mailing = Thread.current[:current_caffeinate_mailing] if Thread.current[:current_caffeinate_mailing]
10
+ @mailing = params[:mailing] if params
9
11
  end
12
+
13
+ klass.helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
14
+ end
15
+
16
+ # Assumes `@mailing` is set
17
+ def caffeinate_unsubscribe_url(mailing: nil, **options)
18
+ mailing ||= @mailing
19
+ Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(mailing.caffeinate_campaign_subscription, **options)
20
+ end
21
+
22
+ # Assumes `@mailing` is set
23
+ def caffeinate_subscribe_url(mailing: nil, **options)
24
+ mailing ||= @mailing
25
+ Caffeinate::UrlHelpers.caffeinate_subscribe_url(mailing.caffeinate_campaign_subscription, **options)
10
26
  end
11
27
  end
12
28
  end
@@ -2,13 +2,15 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
+ # Handles the evaluation of a drip against a mailing to determine if it ultimately gets delivered.
6
+ # Also invokes the `before_send` callbacks.
5
7
  class Interceptor
6
8
  # Handles `before_send` callbacks for a `Caffeinate::Dripper`
7
9
  def self.delivering_email(message)
8
- mailing = Thread.current[:current_caffeinate_mailing]
10
+ mailing = message.caffeinate_mailing
9
11
  return unless mailing
10
12
 
11
- mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing.caffeinate_campaign_subscription, mailing, message)
13
+ mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing, message)
12
14
  drip = mailing.drip
13
15
  message.perform_deliveries = drip.enabled?(mailing)
14
16
  end
@@ -2,15 +2,15 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
- # Handles updating the Caffeinate::Message if it's available in Thread.current[:current_caffeinate_mailing]
5
+ # Handles updating the Caffeinate::Message if it's available in Mail::Message.caffeinate_mailing
6
6
  # and runs any associated callbacks
7
7
  class Observer
8
8
  def self.delivered_email(message)
9
- mailing = Thread.current[:current_caffeinate_mailing]
9
+ mailing = message.caffeinate_mailing
10
10
  return unless mailing
11
11
 
12
- mailing.update!(sent_at: Caffeinate.config.time_now) if message.perform_deliveries
13
- mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing.caffeinate_campaign_subscription, mailing, message)
12
+ mailing.update!(sent_at: Caffeinate.config.time_now, skipped_at: nil) if message.perform_deliveries
13
+ mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing, message)
14
14
  end
15
15
  end
16
16
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActiveRecord
5
+ # Includes the ActiveRecord association and relevant scopes for an ActiveRecord-backed model
5
6
  module Extension
6
7
  # Adds the associations for a subscriber
7
8
  def caffeinate_subscriber
8
- has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription'
9
+ has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription', dependent: :destroy
9
10
  has_many :caffeinate_campaigns, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Campaign'
10
11
  has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Mailing'
11
12
 
12
- scope :not_subscribed_to, lambda { |list|
13
+ scope :not_subscribed_to_campaign, lambda { |list|
13
14
  subscribed = ::Caffeinate::CampaignSubscription.select(:subscriber_id).joins(:caffeinate_campaign).where(caffeinate_campaigns: { slug: list }, subscriber_type: name)
14
15
  where.not(id: subscribed)
15
16
  }
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
+ # Global configuration
4
5
  class Configuration
5
- attr_accessor :now, :async_delivery, :mailing_job
6
+ attr_accessor :now, :async_delivery, :mailing_job, :batch_size, :drippers_path
6
7
 
7
8
  def initialize
8
9
  @now = -> { Time.current }
9
10
  @async_delivery = false
10
11
  @mailing_job = nil
12
+ @batch_size = 1_000
13
+ @drippers_path = "app/drippers"
11
14
  end
12
15
 
13
16
  def now=(val)
@@ -1,40 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'caffeinate/drip_evaluator'
3
4
  module Caffeinate
4
5
  # A Drip object
6
+ #
7
+ # Handles the block and provides convenience methods for the drip
5
8
  class Drip
6
- # Handles the block and provides convenience methods for the drip
7
- class Evaluator
8
- attr_reader :mailing
9
- def initialize(mailing)
10
- @mailing = mailing
11
- end
12
-
13
- def call(&block)
14
- return true unless block
15
-
16
- instance_eval(&block)
17
- end
18
-
19
- # Ends the CampaignSubscription
20
- def end!
21
- mailing.caffeinate_campaign_subscription.end!
22
- false
23
- end
24
-
25
- # Unsubscribes the CampaignSubscription
26
- def unsubscribe!
27
- mailing.caffeinate_campaign_subscription.unsubscribe!
28
- false
29
- end
30
-
31
- # Skips the mailing
32
- def skip!
33
- mailing.skip!
34
- false
35
- end
36
- end
37
-
38
9
  attr_reader :dripper, :action, :options, :block
39
10
  def initialize(dripper, action, options, &block)
40
11
  @dripper = dripper
@@ -43,14 +14,30 @@ module Caffeinate
43
14
  @block = block
44
15
  end
45
16
 
46
- # If the associated ActionMailer uses `ActionMailer::Parameterized` initialization
17
+ # If the associated ActionMailer uses `ActionMailer::Parameterized` initialization instead of argument-based initialization
47
18
  def parameterized?
48
19
  options[:using] == :parameterized
49
20
  end
50
21
 
51
- # If the drip is enabled.
22
+ def send_at(mailing = nil)
23
+ if periodical?
24
+ start = mailing.instance_exec(&options[:start])
25
+ if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count > 0
26
+ start += options[:every]
27
+ end
28
+ start.from_now
29
+ else
30
+ options[:delay].from_now
31
+ end
32
+ end
33
+
34
+ def periodical?
35
+ options[:every].present?
36
+ end
37
+
38
+ # Checks if the drip is enabled
52
39
  def enabled?(mailing)
53
- Evaluator.new(mailing).call(&@block)
40
+ DripEvaluator.new(mailing).call(&@block)
54
41
  end
55
42
  end
56
43
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ # Handles evaluating the `drip` block and provides convenience methods for handling the mailing or its campaign.
5
+ class DripEvaluator
6
+ attr_reader :mailing
7
+ def initialize(mailing)
8
+ @mailing = mailing
9
+ end
10
+
11
+ def call(&block)
12
+ return true unless block
13
+
14
+ instance_eval(&block)
15
+ end
16
+
17
+ # Ends the CampaignSubscription
18
+ def end!
19
+ mailing.caffeinate_campaign_subscription.end!
20
+ false
21
+ end
22
+
23
+ # Unsubscribes the CampaignSubscription
24
+ def unsubscribe!
25
+ mailing.caffeinate_campaign_subscription.unsubscribe!
26
+ false
27
+ end
28
+
29
+ # Skips the mailing
30
+ def skip!
31
+ mailing.skip!
32
+ false
33
+ end
34
+ end
35
+ end
@@ -1,33 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/dripper/drip'
3
+ require 'caffeinate/dripper/batching'
4
4
  require 'caffeinate/dripper/callbacks'
5
- require 'caffeinate/dripper/defaults'
6
- require 'caffeinate/dripper/subscriber'
7
5
  require 'caffeinate/dripper/campaign'
8
- require 'caffeinate/dripper/perform'
6
+ require 'caffeinate/dripper/defaults'
9
7
  require 'caffeinate/dripper/delivery'
8
+ require 'caffeinate/dripper/drip'
9
+ require 'caffeinate/dripper/inferences'
10
+ require 'caffeinate/dripper/perform'
11
+ require 'caffeinate/dripper/periodical'
12
+ require 'caffeinate/dripper/subscriber'
10
13
 
11
14
  module Caffeinate
12
15
  module Dripper
16
+ # Base class
13
17
  class Base
18
+ include Batching
14
19
  include Callbacks
15
20
  include Campaign
16
21
  include Defaults
17
22
  include Delivery
18
23
  include Drip
24
+ include Inferences
19
25
  include Perform
26
+ include Periodical
20
27
  include Subscriber
21
-
22
- # The inferred mailer class
23
- def self.inferred_mailer_class
24
- klass_name = "#{name.delete_suffix('Dripper')}Mailer"
25
- klass = klass_name.safe_constantize
26
- return nil unless klass
27
- return klass_name if klass < ::ActionMailer::Base
28
-
29
- nil
30
- end
31
28
  end
32
29
  end
33
30
  end
31
+
32
+ ActiveSupport.run_load_hooks :caffeinate, Caffeinate::Dripper::Base
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Includes batch support for setting the batch size for Perform
6
+ module Batching
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def batch_size=(num)
13
+ @batch_size = num
14
+ end
15
+
16
+ def batch_size
17
+ @batch_size || ::Caffeinate.config.batch_size
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,12 +2,17 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Callbacks for a Dripper.
5
6
  module Callbacks
6
7
  # :nodoc:
7
8
  def self.included(klass)
8
9
  klass.extend ClassMethods
9
10
  end
10
11
 
12
+ def run_callbacks(name, *args)
13
+ self.class.run_callbacks(name, *args)
14
+ end
15
+
11
16
  module ClassMethods
12
17
  # :nodoc:
13
18
  def run_callbacks(name, *args)
@@ -33,15 +38,64 @@ module Caffeinate
33
38
  @on_subscribe_blocks ||= []
34
39
  end
35
40
 
41
+ # Callback before the mailings get processed.
42
+ #
43
+ # before_process do |dripper|
44
+ # Slack.notify(:caffeinate, "Dripper is getting ready for mailing! #{dripper.caffeinate_campaign.name}!")
45
+ # end
46
+ #
47
+ # @yield Caffeinate::Dripper
48
+ def before_process(&block)
49
+ before_process_blocks << block
50
+ end
51
+
52
+ # :nodoc:
53
+ def before_process_blocks
54
+ @before_process_blocks ||= []
55
+ end
56
+
57
+ # Callback before the mailings get processed in a batch.
58
+ #
59
+ # after_process do |dripper, mailings|
60
+ # Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
61
+ # end
62
+ #
63
+ # @yield Caffeinate::Dripper
64
+ # @yield Caffeinate::Mailing [Array]
65
+ def on_process(&block)
66
+ on_process_blocks << block
67
+ end
68
+
69
+ # :nodoc:
70
+ def on_process_blocks
71
+ @on_process_blocks ||= []
72
+ end
73
+
74
+ # Callback after the all the mailings have been sent.
75
+ #
76
+ # after_process do |dripper|
77
+ # Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
78
+ # end
79
+ #
80
+ # @yield Caffeinate::Dripper
81
+ # @yield Caffeinate::Mailing [Array]
82
+ def after_process(&block)
83
+ after_process_blocks << block
84
+ end
85
+
86
+ # :nodoc:
87
+ def after_process_blocks
88
+ @after_process_blocks ||= []
89
+ end
90
+
36
91
  # Callback before a Drip has called the mailer.
37
92
  #
38
93
  # before_drip do |campaign_subscription, mailing, drip|
39
94
  # Slack.notify(:caffeinate, "#{drip.action_name} is starting")
40
95
  # end
41
96
  #
42
- # @yield Caffeinate::CampaignSubscription
43
- # @yield Caffeinate::Mailing
44
97
  # @yield Caffeinate::Drip current drip
98
+ # @yield Caffeinate::Mailing
45
99
  def before_drip(&block)
46
100
  before_drip_blocks << block
47
101
  end
@@ -57,7 +111,6 @@ module Caffeinate
57
111
  # Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
58
112
  # end
59
113
  #
60
- # @yield Caffeinate::CampaignSubscription
61
114
  # @yield Caffeinate::Mailing
62
115
  # @yield Mail::Message
63
116
  def before_send(&block)
@@ -75,7 +128,6 @@ module Caffeinate
75
128
  # Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
76
129
  # end
77
130
  #
78
- # @yield Caffeinate::CampaignSubscription
79
131
  # @yield Caffeinate::Mailing
80
132
  # @yield Mail::Message
81
133
  def after_send(&block)
@@ -125,8 +177,7 @@ module Caffeinate
125
177
  # Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
126
178
  # end
127
179
  #
128
- # @yield Caffeinate::CampaignSubscription
129
- # @yield Caffeinate::Mailing
180
+ # @yield `Caffeinate::Mailing`
130
181
  def on_skip(&block)
131
182
  on_skip_blocks << block
132
183
  end
@@ -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