caffeinate 0.2.0 → 0.2.1

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