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
@@ -12,7 +12,10 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
12
12
  t.string :user_id
13
13
  t.string :token, null: false
14
14
  t.datetime :ended_at
15
+ t.string :ended_reason
16
+ t.datetime :resubscribed_at
15
17
  t.datetime :unsubscribed_at
18
+ t.string :unsubscribe_reason
16
19
 
17
20
  t.timestamps
18
21
  end
@@ -14,6 +14,9 @@ class CreateCaffeinateMailings < ActiveRecord::Migration[6.0]
14
14
 
15
15
  t.timestamps
16
16
  end
17
- add_index :caffeinate_mailings, %i[campaign_subscription_id mailer_class mailer_action sent_at send_at skipped_at], name: :index_caffeinate_mailings
17
+ add_index :caffeinate_mailings, :sent_at
18
+ add_index :caffeinate_mailings, :send_at
19
+ add_index :caffeinate_mailings, :skipped_at
20
+ add_index :caffeinate_mailings, %i[caffeinate_campaign_subscription_id mailer_class mailer_action], name: :index_caffeinate_mailings
18
21
  end
19
22
  end
@@ -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,16 @@
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
+ true
14
15
  end
15
16
  end
16
17
  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_perform 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_perform(&block)
49
+ before_perform_blocks << block
50
+ end
51
+
52
+ # :nodoc:
53
+ def before_perform_blocks
54
+ @before_perform_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_perform(&block)
66
+ on_perform_blocks << block
67
+ end
68
+
69
+ # :nodoc:
70
+ def on_perform_blocks
71
+ @on_perform_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_perform(&block)
83
+ after_perform_blocks << block
84
+ end
85
+
86
+ # :nodoc:
87
+ def after_perform_blocks
88
+ @after_perform_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)
@@ -119,14 +171,30 @@ module Caffeinate
119
171
  @on_unsubscribe_blocks ||= []
120
172
  end
121
173
 
174
+
175
+ # Callback after a CampaignSubscriber has ended.
176
+ #
177
+ # on_end do |campaign_sub|
178
+ # Slack.notify(:caffeinate, "#{campaign_sub.id} has ended... sad day.")
179
+ # end
180
+ #
181
+ # @yield Caffeinate::CampaignSubscription
182
+ def on_end(&block)
183
+ on_end_blocks << block
184
+ end
185
+
186
+ # :nodoc:
187
+ def on_end_blocks
188
+ @on_end_blocks ||= []
189
+ end
190
+
122
191
  # Callback after a `Caffeinate::Mailing` is skipped.
123
192
  #
124
193
  # on_skip do |campaign_subscription, mailing, message|
125
194
  # Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
126
195
  # end
127
196
  #
128
- # @yield Caffeinate::CampaignSubscription
129
- # @yield Caffeinate::Mailing
197
+ # @yield `Caffeinate::Mailing`
130
198
  def on_skip(&block)
131
199
  on_skip_blocks << block
132
200
  end