caffeinate 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3032e43fad1742429e8961a3bfca1e522c9fcf19714c8cc998d6dea629c30a6e
4
- data.tar.gz: a51f59c2d0767d18002051827590d31a7ab0b96ae570a1c048d0a37ab4446007
3
+ metadata.gz: 3241ca7ca0c6e31220511ad04172105d97058fa84f1de1e6ac442052823a91e0
4
+ data.tar.gz: 5b0bda6f08ae89f79cbd64df4c75f48fcb0dacf7471155aae779290b6a1b2e41
5
5
  SHA512:
6
- metadata.gz: ede225ce8d0fbb71214411d38366ea43992da7b95428734af5c4134f496c04d2dbf2b4a940f19ffbbf24dde9c4536834b96051bd493ec3476897f6f376b2324a
7
- data.tar.gz: acc6124e88a4d8e6beced5afc80e7774de78b60858940e579f821ed172ad14f6a033e7b0419c105c233806f94005f98d4dab73f25bbb53e40175d7dca289c9f2
6
+ metadata.gz: d43a0030851c7fa107fed83fcc05c9cfa8596be068a8327d01f597a0a45fc84aa403d1a95990e8a4c45013fb44bc899215778d36ce8a8f5cc770db50b33cd0df
7
+ data.tar.gz: 6f9e399cc7e0a00bbb544b668d80e0438f4482013f3163a819b9f2ab8ea502f3c8535aacb91d57eb80efc5f12c22bd03a2bdfd36c2216544bb9aa1fd6928e797
data/README.md CHANGED
@@ -1,81 +1,89 @@
1
1
  # Caffeinate
2
2
 
3
- Ruby on Rails drip campaign engine.
3
+ Caffeinate is a drip campaign engine for Ruby on Rails applications.
4
4
 
5
- ## Are there docs?
6
-
7
- [Since you asked](https://rubydoc.info/github/joshmn/caffeinate).
5
+ Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
6
+ and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
7
+ already familiar with: Ruby on Rails.
8
8
 
9
9
  ## Usage
10
10
 
11
- Given a mailer like this:
11
+ You can probably imagine seeing a Mailer like this:
12
12
 
13
13
  ```ruby
14
- class AbandonedCartMailer < ActionMailer::Base
15
- def you_forgot_something(cart)
16
- mail(to: cart.user.email, subject: "You forgot something!")
14
+ class OnboardingMailer < ActionMailer::Base
15
+ # Send on account creation
16
+ def welcome_to_my_cool_app(user)
17
+ mail(to: user.email, subject: "You forgot something!")
17
18
  end
18
19
 
19
- def selling_out_soon(cart)
20
- mail(to: cart.user.email, subject: "Selling out soon!")
20
+ # Send 2 days after the user signs up
21
+ def some_cool_tips(user)
22
+ mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
23
+ end
24
+
25
+ # Sends 3 days after the user signs up and hasn't added a company profile yet
26
+ def help_getting_started(user)
27
+ return if user.company.present?
28
+
29
+ mail(to: user.email, subject: "Did you know...")
21
30
  end
22
31
  end
23
32
  ```
24
33
 
34
+ With background jobs running, checking, and everything else. That's messy. Why are we checking state in the Mailer? Ugh.
35
+
36
+ We can clean this up with Caffeinate. Here's how we'd do it.
37
+
25
38
  ### Create a Campaign
26
39
 
27
40
  ```ruby
28
- Caffeinate::Campaign.create!(name: "Abandoned Cart", slug: "abandoned_cart")
41
+ Caffeinate::Campaign.create!(name: "Onboarding Campaign", slug: "onboarding")
29
42
  ```
30
43
 
31
44
  ### Create a Caffeinate::Dripper
32
45
 
46
+ Place the contents below in `app/drippers/onboarding_dripper.rb`:
47
+
33
48
  ```ruby
34
- class AbandonedCartDripper < Caffeinate::Dripper::Base
35
- # This should match a Caffeinate::Campaign#slug
36
- campaign :abandoned_cart
37
-
38
- # A block to subscribe your users automatically
39
- # You can invoke this by calling `AbandonedCartDripper.subscribe!`,
40
- # probably in a background process, run at a given interval
41
- subscribes do
42
- Cart.left_joins(:cart_items)
43
- .includes(:user)
44
- .where(completed_at: nil)
45
- .where(updated_at: 1.day.ago..2.days.ago)
46
- .having('count(cart_items.id) = 0').each do |cart|
47
- subscribe(cart, user: cart.user)
48
- end
49
- end
50
-
51
- # Register your drips! Syntax is
52
- # drip <mailer_action_name>, mailer: <MailerClass>, delay: <ActiveSupport::Interval>
53
- drip :you_forgot_something, mailer: "AbandonedCartMailer", delay: 1.hour
54
- drip :selling_out_soon, mailer: "AbandonedCartMailer", delay: 8.hours do
55
- cart = mailing.subscriber
56
- if cart.completed?
57
- end! # you can also invoke `unsubscribe!` to cancel this mailing and all future mailings
49
+ class OnboardingDripper < ApplicationDripper
50
+ drip :welcome_to_my_cool_app, delay: 0.hours
51
+ drip :some_cool_tips, delay: 2.days
52
+ drip :help_getting_started, delay: 3.days do
53
+ if mailing.user.company.present?
54
+ mailing.unsubscribe!
58
55
  return false
59
- end
60
- end
56
+ end
57
+ end
61
58
  end
62
59
  ```
63
60
 
64
- Automatically subscribe eligible carts to it by running:
61
+ ### Add a subscriber to the Campaign
65
62
 
66
63
  ```ruby
67
- AbandonedCartDripper.subscribe!
64
+ class User < ApplicationRecord
65
+ after_create_commit do
66
+ Caffeinate::Campaign.find_by(slug: "onboarding").subscribe(self)
67
+ end
68
+ end
68
69
  ```
69
70
 
70
- This would typically run in a background job, queued up at a given interval.
71
+ ### Run the Dripper
71
72
 
72
- And then, once it's done, start your engines!
73
+ You'd normally want to do this in a cron/whenever/scheduled Sidekiq/etc job.
73
74
 
74
- ```ruby
75
- AbandonedCartDripper.perform!
75
+ ```ruby
76
+ OnboardingDripper.perform!
76
77
  ```
77
78
 
78
- This, too, would typically run in a background job, queued up at a given interval.
79
+ ### Spend more time building
80
+
81
+ Now you can spend more time building your app and less time managing your marketing campaigns.
82
+ * Centralized logic makes it easy to understand the flow
83
+ * Subscription management, timings, send history all built-in
84
+ * Built on the stack you're already familiar with
85
+
86
+ There's a lot more than what you just saw, too! Caffeinate almost makes managing timed email sequences fun.
79
87
 
80
88
  ## Installation
81
89
 
@@ -103,6 +111,11 @@ Followed by a migrate:
103
111
  $ rails db:migrate
104
112
  ```
105
113
 
114
+ ## Documentation
115
+
116
+ * [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
117
+ * [Better-than-average code documentation](https://rubydoc.info/github/joshmn/caffeinate)
118
+
106
119
  ## Upcoming features/todo
107
120
 
108
121
  * Ability to optionally use relative start time when creating a step
@@ -22,9 +22,38 @@ module Caffeinate
22
22
  Caffeinate.dripper_to_campaign_class[slug.to_sym].constantize
23
23
  end
24
24
 
25
+ # Convenience method for find_by!(slug: value)
26
+ #
27
+ # ::Caffeinate::Campaign[:onboarding]
28
+ # # is the same as
29
+ # ::Caffeinate::Campaign.find_by(slug: :onboarding)
30
+ def self.[](val)
31
+ find_by!(slug: val)
32
+ end
33
+
34
+ # Checks to see if the subscriber exists.
35
+ #
36
+ # Use `find_by` so that we don't have to load the record twice. Often used with `subscribes?`
37
+ def subscriber(record, **args)
38
+ @subscriber ||= caffeinate_campaign_subscriptions.find_by(subscriber: record, **args)
39
+ end
40
+
41
+ # Check if the subscriber exists
42
+ def subscribes?(record, **args)
43
+ subscriber(record, **args).present?
44
+ end
45
+
25
46
  # Subscribes an object to a campaign.
26
47
  def subscribe(subscriber, **args)
27
48
  caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
28
49
  end
50
+
51
+ # Subscribes an object to a campaign.
52
+ def subscribe!(subscriber, **args)
53
+ subscription = subscribe(subscriber, **args)
54
+ return subscription if subscribe.persisted?
55
+
56
+ raise ActiveRecord::RecordInvalid, subscription
57
+ end
29
58
  end
30
59
  end
@@ -22,8 +22,8 @@ module Caffeinate
22
22
  class CampaignSubscription < ApplicationRecord
23
23
  self.table_name = 'caffeinate_campaign_subscriptions'
24
24
 
25
- has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
26
- has_one :next_caffeinate_mailing, -> { upcoming.unsent.limit(1).first }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
25
+ has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
26
+ has_one :next_caffeinate_mailing, -> { upcoming.unsent }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
27
27
  belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
28
28
  belongs_to :subscriber, polymorphic: true
29
29
  belongs_to :user, polymorphic: true, optional: true
@@ -78,6 +78,13 @@ module Caffeinate
78
78
  caffeinate_campaign.to_dripper.run_callbacks(:on_unsubscribe, self)
79
79
  end
80
80
 
81
+ # Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks
82
+ def resubscribe!
83
+ update!(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
84
+
85
+ caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
86
+ end
87
+
81
88
  private
82
89
 
83
90
  # Create mailings according to the drips registered in the Campaign
@@ -17,7 +17,7 @@
17
17
  module Caffeinate
18
18
  # Records of the mails sent and to be sent for a given `::Caffeinate::CampaignSubscriber`
19
19
  class Mailing < ApplicationRecord
20
- CURRENT_THREAD_KEY = :current_caffeinate_mailing.freeze
20
+ CURRENT_THREAD_KEY = :current_caffeinate_mailing
21
21
 
22
22
  self.table_name = 'caffeinate_mailings'
23
23
 
@@ -12,6 +12,7 @@ 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.datetime :resubscribed_at
15
16
  t.datetime :unsubscribed_at
16
17
 
17
18
  t.timestamps
@@ -28,4 +28,14 @@ module Caffeinate
28
28
  def self.setup
29
29
  yield config
30
30
  end
31
+
32
+ # The current mailing
33
+ def self.current_mailing=(val)
34
+ Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY] = val
35
+ end
36
+
37
+ # The current mailing
38
+ def self.current_mailing
39
+ Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
40
+ end
31
41
  end
@@ -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,21 +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[::Caffeinate::Mailing::CURRENT_THREAD_KEY] if Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
10
+ @mailing = Caffeinate.current_mailing if Caffeinate.current_mailing
9
11
  end
10
12
 
11
13
  klass.helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
12
14
  end
13
15
 
14
- def caffeinate_unsubscribe_url(**options)
15
- Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(@mailing.caffeinate_campaign_subscription, **options)
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)
16
20
  end
17
21
 
18
- def caffeinate_subscribe_url
19
- Caffeinate::UrlHelpers.caffeinate_subscribe_url(@mailing.caffeinate_campaign_subscription, **options)
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)
20
26
  end
21
27
  end
22
28
  end
@@ -2,10 +2,12 @@
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[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
10
+ mailing = Caffeinate.current_mailing
9
11
  return unless mailing
10
12
 
11
13
  mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing.caffeinate_campaign_subscription, mailing, message)
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
- # Handles updating the Caffeinate::Message if it's available in Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
5
+ # Handles updating the Caffeinate::Message if it's available in Caffeinate.current_mailing
6
6
  # and runs any associated callbacks
7
7
  class Observer
8
8
  def self.delivered_email(message)
9
- mailing = Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
9
+ mailing = Caffeinate.current_mailing
10
10
  return unless mailing
11
11
 
12
12
  mailing.update!(sent_at: Caffeinate.config.time_now, skipped_at: nil) if message.perform_deliveries
@@ -2,10 +2,11 @@
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
 
@@ -1,13 +1,15 @@
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
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
11
13
  end
12
14
 
13
15
  def now=(val)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Caffeinate
2
4
  # Handles evaluating the `drip` block and provides convenience methods for handling the mailing or its campaign.
3
5
  class DripEvaluator
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/dripper/drip'
4
- require 'caffeinate/dripper/inferences'
3
+ require 'caffeinate/dripper/batching'
5
4
  require 'caffeinate/dripper/callbacks'
6
- require 'caffeinate/dripper/defaults'
7
- require 'caffeinate/dripper/subscriber'
8
5
  require 'caffeinate/dripper/campaign'
9
- require 'caffeinate/dripper/perform'
6
+ require 'caffeinate/dripper/defaults'
10
7
  require 'caffeinate/dripper/delivery'
8
+ require 'caffeinate/dripper/drip'
9
+ require 'caffeinate/dripper/inferences'
10
+ require 'caffeinate/dripper/perform'
11
+ require 'caffeinate/dripper/subscriber'
11
12
 
12
13
  module Caffeinate
13
14
  module Dripper
15
+ # Base class
14
16
  class Base
17
+ include Batching
15
18
  include Callbacks
16
19
  include Campaign
17
20
  include Defaults
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ # Includes batch support for setting the batch size for Perform
5
+ module Batching
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def batch_size(num)
12
+ @_batch_size = num
13
+ end
14
+
15
+ def _batch_size
16
+ @_batch_size || ::Caffeinate.config.batch_size
17
+ end
18
+ end
19
+ end
20
+ 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,6 +38,56 @@ 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|
@@ -125,8 +180,8 @@ module Caffeinate
125
180
  # Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
126
181
  # end
127
182
  #
128
- # @yield Caffeinate::CampaignSubscription
129
- # @yield Caffeinate::Mailing
183
+ # @yield `Caffeinate::CampaignSubscription`
184
+ # @yield `Caffeinate::Mailing`
130
185
  def on_skip(&block)
131
186
  on_skip_blocks << block
132
187
  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
  #
@@ -33,14 +34,11 @@ module Caffeinate
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
45
43
 
46
44
  # The defined slug or the inferred slug
@@ -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,7 @@ 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.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
27
28
  @defaults = options
28
29
  end
29
30
  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,7 +14,7 @@ module Caffeinate
14
14
  #
15
15
  # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
16
  def deliver!(mailing)
17
- Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY] = mailing
17
+ Caffeinate.current_mailing = mailing
18
18
 
19
19
  if mailing.drip.parameterized?
20
20
  mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action).deliver
@@ -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
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Caffeinate
2
4
  module Dripper
5
+ # Includes the inferred methods based on a Dripper name.
3
6
  module Inferences
4
7
  def self.included(klass)
5
8
  klass.extend ClassMethods
@@ -18,7 +21,7 @@ module Caffeinate
18
21
 
19
22
  # The inferred mailer class
20
23
  def inferred_campaign_slug
21
- "#{name.delete_suffix('Dripper')}".underscore
24
+ name.delete_suffix('Dripper').to_s.underscore
22
25
  end
23
26
  end
24
27
  end
@@ -2,7 +2,7 @@
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)
@@ -11,16 +11,21 @@ module Caffeinate
11
11
 
12
12
  # Delivers the next_caffeinate_mailer for the campaign's subscribers.
13
13
  #
14
+ # Handles with batches based on batch_size.
15
+ #
14
16
  # OrderDripper.new.perform!
15
17
  #
16
18
  # @return nil
17
19
  def perform!
18
- campaign.caffeinate_campaign_subscriptions.active.includes(:next_caffeinate_mailing).each do |subscriber|
19
- if subscriber.next_caffeinate_mailing
20
- subscriber.next_caffeinate_mailing.process!
20
+ run_callbacks(:before_process, self)
21
+ campaign.caffeinate_campaign_subscriptions.active.in_batches(of: self.class._batch_size).each do |batch|
22
+ run_callbacks(:on_process, self, batch)
23
+ batch.each do |subscriber|
24
+ subscriber.next_caffeinate_mailing&.process!
21
25
  end
22
26
  end
23
- true
27
+ run_callbacks(:after_process, self)
28
+ nil
24
29
  end
25
30
 
26
31
  module ClassMethods
@@ -5,15 +5,10 @@ require 'caffeinate/action_mailer'
5
5
  require 'caffeinate/active_record/extension'
6
6
 
7
7
  module Caffeinate
8
- # :nodoc:
8
+ # Adds Caffeinate to Rails
9
9
  class Engine < ::Rails::Engine
10
10
  isolate_namespace Caffeinate
11
-
12
- config.to_prepare do
13
- Dir.glob(Rails.root.join("app/drippers/**/*.rb")).each do |file|
14
- require file
15
- end
16
- end
11
+ config.eager_load_namespaces << Caffeinate
17
12
 
18
13
  ActiveSupport.on_load(:action_mailer) do
19
14
  include ::Caffeinate::ActionMailer::Extension
@@ -26,7 +21,7 @@ module Caffeinate
26
21
  end
27
22
 
28
23
  ActiveSupport.on_load(:action_view) do
29
- ApplicationHelper.send(:include, ::Caffeinate::Helpers)
24
+ ApplicationHelper.include ::Caffeinate::Helpers
30
25
  end
31
26
  end
32
27
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Caffeinate
4
+ # URL helpers for accessing the mounted Caffeinate instance.
2
5
  module Helpers
3
6
  def caffeinate_unsubscribe_url(subscription, **options)
4
7
  opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'caffeinate/helpers'
2
4
 
3
5
  module Caffeinate
6
+ # Convenience class for the URL helpers.
4
7
  class UrlHelpers
5
8
  extend Helpers
6
9
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.1'
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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caffeinate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Brody
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-27 00:00:00.000000000 Z
11
+ date: 2020-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -135,7 +135,6 @@ files:
135
135
  - lib/caffeinate.rb
136
136
  - lib/caffeinate/action_mailer.rb
137
137
  - lib/caffeinate/action_mailer/extension.rb
138
- - lib/caffeinate/action_mailer/helpers.rb
139
138
  - lib/caffeinate/action_mailer/interceptor.rb
140
139
  - lib/caffeinate/action_mailer/observer.rb
141
140
  - lib/caffeinate/active_record/extension.rb
@@ -144,6 +143,7 @@ files:
144
143
  - lib/caffeinate/drip.rb
145
144
  - lib/caffeinate/drip_evaluator.rb
146
145
  - lib/caffeinate/dripper/base.rb
146
+ - lib/caffeinate/dripper/batching.rb
147
147
  - lib/caffeinate/dripper/callbacks.rb
148
148
  - lib/caffeinate/dripper/campaign.rb
149
149
  - lib/caffeinate/dripper/defaults.rb
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Caffeinate
4
- module ActionMailer
5
- module Helpers
6
- def caffeinate_unsubscribe_url(caffeinate_campaign_subscription, **options)
7
- opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
8
- Caffeinate::Engine.routes.url_helpers.caffeinate_unsubscribe_url(token: caffeinate_campaign_subscription.token, **opts)
9
- end
10
- end
11
- end
12
- end