caffeinate 0.14.0 → 2.0.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: a8ae5ac5375188ce5a7bc8960ac7d7b392ccc7dc7de71070e88a2285fa76e7af
4
- data.tar.gz: dd06d837c2c1477bf8e48484e028016f00203066f0b2383c6f0a7a3cb5b5af1f
3
+ metadata.gz: f16a6861b725b37b7b98bda070586ae794875a171af2b7ea4582a05b7d9aacaa
4
+ data.tar.gz: c6ff565b6792fdf4ed193d3bd478b445a55835262d11118530f69d1aef08e8af
5
5
  SHA512:
6
- metadata.gz: 5c71b3ba2196a9a9f7ae66074b8eeb76ad6fe1a6b2ecff521e7f92c06da411c4722e0bc8b4a49fcfc9eaf0fde921c76fd4b13fbb5c7b59cfe1ec173ea69e5b29
7
- data.tar.gz: 498848318785a2f8f4ab669cada385760f63663b1c6bf254235a7f640c189eaa170391aff58990c05c6a4d8ee6a6dc860399ea24745431dec7dfebaa8d760533
6
+ metadata.gz: c2e5c6cc7c7af6f37c270f883d74e1eff9f48ac4b16226b4bf3c31729c116d7790ba198f7657a272458cf66d9993a2814e8bf2e183fd8830876eee50bf26ba65
7
+ data.tar.gz: 7c9519545b4183787b71422b706a059bab5b73df0f872677f9fdb2dc0f10f60f4a3c2eacbc64eabf1246815bd5574dcadc0750d30a018075945e87bbc9f25254
data/README.md CHANGED
@@ -2,18 +2,32 @@
2
2
  <img width="450" src="https://github.com/joshmn/caffeinate/raw/master/logo.png" alt="Caffeinate logo" />
3
3
  </div>
4
4
 
5
- ---
5
+ <div align="center">
6
+ <a href="https://codecov.io/gh/joshmn/caffeinate">
7
+ <img src="https://codecov.io/gh/joshmn/caffeinate/branch/master/graph/badge.svg?token=5LCOB4ESHL" alt="Coverage"/>
8
+ </a>
9
+ <a href="https://codeclimate.com/github/joshmn/caffeinate/maintainability">
10
+ <img src="https://api.codeclimate.com/v1/badges/9c075416ce74985d5c6c/maintainability" alt="Maintainability"/>
11
+ </a>
12
+ <a href="https://inch-ci.org/github/joshmn/caffeinate">
13
+ <img src="https://inch-ci.org/github/joshmn/caffeinate.svg?branch=master" alt="Docs"/>
14
+ </a>
15
+ </div>
6
16
 
7
17
  # Caffeinate
8
18
 
9
- Caffeinate is a drip campaign engine for Ruby on Rails applications.
19
+ Caffeinate is a drip email engine for managing, creating, and sending scheduled email sequences from your Ruby on Rails application.
10
20
 
11
- Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
12
- and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
13
- already familiar with: Ruby on Rails.
21
+ Caffeinate provides a simple DSL to create scheduled email sequences which can be used by ActionMailer without any additional configuration.
14
22
 
15
23
  There's a cool demo with all the things included at [caffeinate.email](https://caffeinate.email). You can view the [marketing site source code here](https://github.com/joshmn/caffeinate-marketing).
16
24
 
25
+ ## Is this thing dead?
26
+
27
+ No! Not at all!
28
+
29
+ There's not a lot of activity here because it's stable and working! I am more than happy to entertain new features.
30
+
17
31
  ## Do you suffer from ActionMailer tragedies?
18
32
 
19
33
  If you have _anything_ like this is your codebase, **you need Caffeinate**:
@@ -30,19 +44,16 @@ end
30
44
 
31
45
  ```ruby
32
46
  class OnboardingMailer < ActionMailer::Base
33
- # Send on account creation
34
47
  def welcome_to_my_cool_app(user)
35
48
  mail(to: user.email, subject: "Welcome to CoolApp!")
36
49
  end
37
50
 
38
- # Send 2 days after the user signs up
39
51
  def some_cool_tips(user)
40
52
  return if user.unsubscribed_from_onboarding_campaign?
41
53
 
42
54
  mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
43
55
  end
44
56
 
45
- # Sends 3 days after the user signs up and hasn't added a company profile yet
46
57
  def help_getting_started(user)
47
58
  return if user.unsubscribed_from_onboarding_campaign?
48
59
  return if user.onboarding_completed?
@@ -56,22 +67,12 @@ end
56
67
 
57
68
  * You're checking state in a mailer
58
69
  * The unsubscribe feature is, most likely, tied to a `User`, which means...
59
- * It's going to be _so fun_ to scale horizontally
70
+ * It's going to be _so fun_ to scale when you finally want to add more unsubscribe links for different types of sequences
71
+ - "one of your projects has expired", but which one? Then you have to add a column to `projects` and manage all that state... ew
60
72
 
61
- ## Caffeinate to the rescue
73
+ ## Do this all better in five minutes
62
74
 
63
- Caffeinate combines a simple scheduling DSL, ActionMailer, and your data models to create scheduled email sequences.
64
-
65
- What can you do with drip campaigns?
66
- * Onboard new customers with cool tips and tricks
67
- * Remind customers to use your product
68
- * Nag customers about using your product
69
- * Reach their spam folder after you fail to handle their unsubscribe request
70
- * And more!
71
-
72
- ## Onboarding in Caffeinate
73
-
74
- In five minutes you can implement this onboarding campaign, and it won't even hijack your entire app!
75
+ In five minutes you can implement this onboarding campaign:
75
76
 
76
77
  ### Install it
77
78
 
@@ -83,11 +84,11 @@ $ rails g caffeinate:install
83
84
  $ rake db:migrate
84
85
  ```
85
86
 
86
- ### Remove that ActionMailer logic
87
+ ### Clean up the mailer logic
87
88
 
88
- Just delete it. Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
89
+ Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
89
90
 
90
- The only other change you need to make is the argument that the mailer action receives:
91
+ The only other change you need to make is the argument that the mailer action receives. It will now receive a `Caffeinate::Mailing`. [Learn more about the data models](docs/2-data-models.md):
91
92
 
92
93
  ```ruby
93
94
  class OnboardingMailer < ActionMailer::Base
@@ -108,27 +109,36 @@ class OnboardingMailer < ActionMailer::Base
108
109
  end
109
110
  ```
110
111
 
111
- While we're there, let's add an unsubscribe link to the views or layout:
112
-
113
- ```erb
114
- <%= link_to "Stop receiving onboarding tips :(", caffeinate_unsubscribe_url %>
115
- ```
116
-
117
112
  ### Create a Dripper
118
113
 
119
- A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.
114
+ A Dripper has all the logic for your sequence and coordinates with ActionMailer on what to send.
120
115
 
121
116
  In `app/drippers/onboarding_dripper.rb`:
122
117
 
123
118
  ```ruby
124
119
  class OnboardingDripper < ApplicationDripper
120
+ # each sequence is a campaign. This will dynamically create one by the given slug
121
+ self.campaign = :onboarding
122
+
123
+ # gets called before every time we process a drip
124
+ before_drip do |_drip, mailing|
125
+ if mailing.subscription.subscriber.onboarding_completed?
126
+ mailing.subscription.unsubscribe!("Completed onboarding")
127
+ throw(:abort)
128
+ end
129
+ end
130
+
131
+ # map drips to the mailer
125
132
  drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
126
133
  drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
127
134
  drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
128
135
  end
129
136
  ```
130
137
 
131
- The `drip` syntax is `def drip(mailer_action, options = {})`.
138
+ We want to skip sending the `mailing` if the `subscriber` (`User`) completed onboarding. Let's unsubscribe
139
+ with `#unsubscribe!` and give it an optional reason of `Completed onboarding` so we can reference it later
140
+ when we look at analytics. `throw(:abort)` halts the callback chain just like regular Rails callbacks, stopping the
141
+ mailing from being sent.
132
142
 
133
143
  ### Add a subscriber to the Campaign
134
144
 
@@ -138,48 +148,37 @@ a `Caffeinate::CampaignSubscription`.
138
148
  ```ruby
139
149
  class User < ApplicationRecord
140
150
  after_commit on: :create do
141
- OnboardingDripper.subscribe(self)
142
- end
143
-
144
- after_commit on: :update do
145
- if onboarding_completed? && onboarding_completed_changed?
146
- if OnboardingDripper.subscribed?(self)
147
- OnboardingDripper.unsubscribe(self)
148
- end
149
- end
151
+ OnboardingDripper.subscribe!(self)
150
152
  end
151
153
  end
152
154
  ```
153
155
 
154
- When a `Caffeinate::CampaignSubscription` is created, the relevant Dripper is parsed and `Caffeinate::Mailing` records
155
- are created from the `drip` DSL. A `Caffeinate::Mailing` record has a `send_at` attribute which tells Caffeinate when we
156
- can send the mail, which we get from `Caffeiate::Mailing#mailer_class` and `Caffeinate::Mailing#mailer_action`.
157
-
158
156
  ### Run the Dripper
159
157
 
160
- Running `OnboardingDripper.perform!` every `x` minutes will call `Caffeinate::Mailing#process!` on `Caffeinate::Mailing`
161
- records that have `send_at < Time.now`.
162
-
163
158
  ```ruby
164
159
  OnboardingDripper.perform!
165
160
  ```
166
161
 
167
- ### Done. But wait, there's more fun if you want
162
+ ### Done
168
163
 
169
- * Automatic subscriptions
170
- * Campaign-specific unsubscribe links
171
- * Reasons for unsubscribing so you can have some sort of analytics
172
- * Periodical emails (daily, weekly, monthly digests, anyone?)
173
- * Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
164
+ You're done.
174
165
 
175
- ### Done. But wait, there's more fun if you want
166
+ [Check out the docs](/docs/README.md) for a more in-depth guide that includes all the options you can use for more complex setups,
167
+ tips, tricks, and shortcuts.
176
168
 
177
- * Automatic subscriptions
178
- * Campaign-specific unsubscribe links
179
- * Reasons for unsubscribing so you can have some sort of analytics
180
- * Periodical emails (daily, weekly, monthly digests, anyone?)
181
- * Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
169
+ ## But wait, there's more
182
170
 
171
+ Caffeinate also...
172
+
173
+ * ✅ Allows hyper-precise scheduled times. 9:19AM _in the user's timezone_? Sure! **Only on business days**? YES!
174
+ * ✅ Periodicals
175
+ * ✅ Manages unsubscribes
176
+ * ✅ Works with singular and multiple associations
177
+ * ✅ Compatible with every background processor
178
+ * ✅ Tested against large databases at AngelList and is performant as hell
179
+ * ✅ Effortlessly handles complex workflows
180
+ - Need to skip a certain mailing? You can!
181
+
183
182
  ## Documentation
184
183
 
185
184
  * [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
@@ -191,7 +190,7 @@ OnboardingDripper.perform!
191
190
 
192
191
  ## Alternatives
193
192
 
194
- Not a fan? There are some alternatives!
193
+ Not a fan of Caffeinate? I built it because I wasn't a fan of the alternatives. To each their own:
195
194
 
196
195
  * https://github.com/honeybadger-io/heya
197
196
  * https://github.com/tarr11/dripper
@@ -199,11 +198,14 @@ Not a fan? There are some alternatives!
199
198
 
200
199
  ## Contributing
201
200
 
202
- Just do it.
201
+ There's so much more that can be done with this. I'd love to see what you're thinking.
202
+
203
+ If you have general feedback, I'd love to know what you're using Caffeinate for! Please email me (any-thing [at] josh.mn) or [tweet me @joshmn](https://twitter.com/joshmn) or create an issue! I'd love to chat.
203
204
 
204
205
  ## Contributors & thanks
205
206
 
206
207
  * Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
208
+ * Thanks to [markokajzer](https://github.com/markokajzer) for listening to me talk about this most mornings.
207
209
 
208
210
  ## License
209
211
 
@@ -14,12 +14,15 @@ module Caffeinate
14
14
  # Campaign ties together subscribers and mailings, and provides one core model for handling your Drippers.
15
15
  class Campaign < ApplicationRecord
16
16
  self.table_name = 'caffeinate_campaigns'
17
+ class NoSubscription < ::ActiveRecord::RecordInvalid; end
17
18
 
18
19
  has_many :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
19
20
  has_many :subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
20
21
  has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
21
22
  has_many :mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
22
23
 
24
+ scope :active, -> { where(active: true) }
25
+
23
26
  # Poorly-named Campaign class resolver
24
27
  def to_dripper
25
28
  ::Caffeinate.dripper_collection.resolve(self)
@@ -64,7 +67,7 @@ module Caffeinate
64
67
  def unsubscribe!(subscriber, **args)
65
68
  reason = args.delete(:reason)
66
69
  subscription = subscriber(subscriber, **args)
67
- raise ::ActiveRecord::RecordInvalid, subscription if subscription.nil?
70
+ raise NoSubscription, subscription if subscription.nil?
68
71
 
69
72
  subscription.unsubscribe!(reason)
70
73
  end
@@ -72,12 +75,12 @@ module Caffeinate
72
75
  # Creates a `CampaignSubscription` object for the present Campaign. Allows passing `**args` to
73
76
  # delegate additional arguments to the record. Uses `find_or_create_by`.
74
77
  def subscribe(subscriber, **args)
75
- caffeinate_campaign_subscriptions.create(subscriber: subscriber, **args)
78
+ caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
76
79
  end
77
80
 
78
81
  # Subscribes an object to a campaign. Raises `ActiveRecord::RecordInvalid` if the record was invalid.
79
82
  def subscribe!(subscriber, **args)
80
- caffeinate_campaign_subscriptions.create!(subscriber: subscriber, **args)
83
+ caffeinate_campaign_subscriptions.find_or_create_by!(subscriber: subscriber, **args)
81
84
  end
82
85
  end
83
86
  end
@@ -55,7 +55,7 @@ module Caffeinate
55
55
 
56
56
  before_validation :call_dripper_before_subscribe_blocks!, on: :create
57
57
 
58
- after_commit :create_mailings!, on: :create
58
+ after_create :create_mailings!
59
59
 
60
60
  after_commit :on_complete, if: :completed?
61
61
 
@@ -131,18 +131,7 @@ module Caffeinate
131
131
  true
132
132
  end
133
133
 
134
- # Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks.
135
- # Use `force` to forcefully reset. Does not create the mailings.
136
- def resubscribe!(force = false)
137
- return false if ended? && !force
138
- return false if unsubscribed? && !force
139
-
140
- result = update(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
141
-
142
- caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
143
- result
144
- end
145
-
134
+ # Checks if the record is not new and if mailings are all gone.
146
135
  def completed?
147
136
  caffeinate_mailings.unsent.count.zero?
148
137
  end
@@ -24,7 +24,7 @@ module Caffeinate
24
24
  has_one :caffeinate_campaign, through: :caffeinate_campaign_subscription
25
25
  alias_attribute :campaign, :caffeinate_campaign
26
26
 
27
- scope :upcoming, -> { unsent.unskipped.where('send_at < ?', ::Caffeinate.config.time_now).order('send_at asc') }
27
+ scope :upcoming, -> { joins(:caffeinate_campaign_subscription).where(caffeinate_campaign_subscription: ::Caffeinate::CampaignSubscription.active).unsent.unskipped.where('send_at < ?', ::Caffeinate.config.time_now).order('send_at asc') }
28
28
  scope :unsent, -> { unskipped.where(sent_at: nil) }
29
29
  scope :sent, -> { unskipped.where.not(sent_at: nil) }
30
30
  scope :skipped, -> { where.not(skipped_at: nil) }
@@ -112,7 +112,7 @@ module Caffeinate
112
112
 
113
113
  # Delivers the Mailing in the background
114
114
  def deliver_later!
115
- klass = ::Caffeinate.config.mailing_job_class
115
+ klass = ::Caffeinate.config.async_delivery_class
116
116
  if klass.respond_to?(:perform_later)
117
117
  klass.perform_later(id)
118
118
  elsif klass.respond_to?(:perform_async)
@@ -1,3 +1,3 @@
1
1
  <%= t("caffeinate.campaign_subscriptions.subscribe") %>
2
2
  <p><%= t("caffeinate.campaign_subscriptions.changed_your_mind")%></p>
3
- <p><%= link_to "Unsubscribe", caffeinate_subscribe_url %></p>
3
+ <p><%= link_to "Unsubscribe", caffeinate_unsubscribe_url %></p>
@@ -1,3 +1,3 @@
1
1
  <h5><%= t("caffeinate.campaign_subscriptions.unsubscribe")%></h5>
2
2
  <p><%= t("caffeinate.campaign_subscriptions.changed_your_mind")%></p>
3
- <p><%= link_to "Resubscribe", caffeinate_subscribe_url %></p>
3
+ <p><%= link_to "Resubscribe", caffeinate_resubscribe_url %></p>
@@ -3,14 +3,44 @@
3
3
  module Caffeinate
4
4
  # Global configuration
5
5
  class Configuration
6
- attr_accessor :now, :async_delivery, :deliver_later,:mailing_job, :batch_size, :drippers_path, :implicit_campaigns,
7
- :default_ended_reason, :default_unsubscribe_reason
6
+
7
+ # Used for relation to a lot of things. If you have a weird time setup, set this.
8
+ # Accepts anything that responds to `#call`; you'll probably use a block.
9
+ attr_accessor :now
10
+
11
+ # If true, enqueues the processing of a `Caffeinate::Mailing` to the background worker class
12
+ # as defined in `async_delivery_class`
13
+ #
14
+ # Default is false
15
+ attr_accessor :async_delivery
16
+
17
+ # The background worker class for `async_delivery`.
18
+ attr_accessor :async_delivery_class
19
+
20
+ # If true, uses `deliver_later` instead of `deliver`
21
+ attr_accessor :deliver_later
22
+
23
+ # The number of `Caffeinate::Mailing` records we find in a batch at once.
24
+ attr_accessor :batch_size
25
+
26
+ # The path to the drippers
27
+ attr_accessor :drippers_path
28
+
29
+ # Automatically creates a `Caffeinate::Campaign` record by the named slug of the campaign from a dripper
30
+ # if none is found by the slug.
31
+ attr_accessor :implicit_campaigns
32
+
33
+ # The default reason for an ended `Caffeinate::CampaignSubscription`
34
+ attr_accessor :default_ended_reason
35
+
36
+ # The default reason for an unsubscribed `Caffeinate::CampaignSubscription`
37
+ attr_accessor :default_unsubscribe_reason
8
38
 
9
39
  def initialize
10
40
  @now = -> { Time.current }
11
41
  @async_delivery = false
12
42
  @deliver_later = false
13
- @mailing_job = nil
43
+ @async_delivery_class = nil
14
44
  @batch_size = 1_000
15
45
  @drippers_path = 'app/drippers'
16
46
  @implicit_campaigns = true
@@ -24,29 +54,24 @@ module Caffeinate
24
54
  @now = val
25
55
  end
26
56
 
27
- # Automatically create a `::Caffeinate::Campaign` object if not found per `Dripper.inferred_campaign_slug`
28
57
  def implicit_campaigns?
29
- @implicit_campaigns == true
58
+ @implicit_campaigns
30
59
  end
31
60
 
32
- # The current time, for database calls
33
61
  def time_now
34
62
  @now.call
35
63
  end
36
64
 
37
- # If delivery is asyncronous
38
65
  def async_delivery?
39
66
  @async_delivery
40
67
  end
41
68
 
42
- # If we should use `#deliver_later` instead of `#deliver`
43
69
  def deliver_later?
44
70
  @deliver_later
45
71
  end
46
72
 
47
- # The @mailing_job constantized. Only used if `async_delivery = true`
48
- def mailing_job_class
49
- @mailing_job.constantize
73
+ def async_delivery_class
74
+ @async_delivery_class.constantize
50
75
  end
51
76
  end
52
77
  end
@@ -10,7 +10,7 @@ module Caffeinate
10
10
  #
11
11
  # To use this, make sure your initializer is configured correctly:
12
12
  # config.async_delivery = true
13
- # config.mailing_job = 'MyWorker'
13
+ # config.async_delivery_class = 'MyWorker'
14
14
  module DeliverAsync
15
15
  def perform(mailing_id)
16
16
  mailing = ::Caffeinate::Mailing.find(mailing_id)
@@ -1,31 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'caffeinate/drip_evaluator'
4
+ require 'caffeinate/schedule_evaluator'
5
+
4
6
  module Caffeinate
5
7
  # A Drip object
6
8
  #
7
9
  # Handles the block and provides convenience methods for the drip
8
10
  class Drip
9
- class OptionEvaluator
10
- def initialize(thing, drip, mailing)
11
- @thing = thing
12
- @drip = drip
13
- @mailing = mailing
14
- end
15
-
16
- def call
17
- if @thing.is_a?(Symbol)
18
- @drip.dripper.new.send(@thing, @drip, @mailing)
19
- elsif @thing.is_a?(Proc)
20
- @mailing.instance_exec(&@thing)
21
- elsif @thing.is_a?(String)
22
- Time.parse(@thing)
23
- else
24
- @thing
25
- end
26
- end
27
- end
28
-
29
11
  attr_reader :dripper, :action, :options, :block
30
12
 
31
13
  def initialize(dripper, action, options, &block)
@@ -41,29 +23,7 @@ module Caffeinate
41
23
  end
42
24
 
43
25
  def send_at(mailing = nil)
44
- if periodical?
45
- start = mailing.instance_exec(&options[:start])
46
- start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
47
- date = start.from_now
48
- elsif options[:on]
49
- date = OptionEvaluator.new(options[:on], self, mailing).call
50
- else
51
- date = OptionEvaluator.new(options[:delay], self, mailing).call
52
- if date.respond_to?(:from_now)
53
- date = date.from_now
54
- end
55
- end
56
-
57
- if options[:at]
58
- time = OptionEvaluator.new(options[:at], self, mailing).call
59
- return date.change(hour: time.hour, min: time.min, sec: time.sec)
60
- end
61
-
62
- date
63
- end
64
-
65
- def periodical?
66
- options[:every].present?
26
+ ::Caffeinate::ScheduleEvaluator.call(self, mailing)
67
27
  end
68
28
 
69
29
  # Checks if the drip is enabled
@@ -30,7 +30,22 @@ module Caffeinate
30
30
  # @option options [String] :mailer_class The mailer_class
31
31
  # @option options [Integer] :step The order in which the drip is executed
32
32
  # @option options [ActiveSupport::Duration] :delay When the drip should be ran
33
- # @option options [Symbol] :using set to :parameters if the mailer action uses ActionMailer::Parameters
33
+ # @option options [Block|Symbol|String] :at Alternative to `:delay` option, allowing for more fine-tuned timing.
34
+ # Accepts a block, symbol (an instance method on the Dripper that accepts two arguments: drip, mailing), or a string
35
+ # to be later parsed into a Time object.
36
+ #
37
+ # drip :mailer_action_name, mailer_class: "MailerClass", at: -> { 3.days.from_now.in_time_zone(mailing.subscriber.timezone) }
38
+ #
39
+ # class MyDripper
40
+ # drip :mailer_action_name, mailer_class: "MailerClass", at: :generate_date
41
+ # def generate_date(drip, mailing)
42
+ # 3.days.from_now.in_time_zone(mailing.subscriber.timezone)
43
+ # end
44
+ # end
45
+ #
46
+ # drip :mailer_action_name, mailer_class: "MailerClass", at: 'January 1, 2022'
47
+ #
48
+ # @option options [Symbol] :using Set to `:parameters` if the mailer action uses ActionMailer::Parameters
34
49
  def drip(action_name, options = {}, &block)
35
50
  drip_collection.register(action_name, options, &block)
36
51
  end
@@ -4,6 +4,8 @@ module Caffeinate
4
4
  module Dripper
5
5
  # A collection of Drip objects for a `Caffeinate::Dripper`
6
6
  class DripCollection
7
+ VALID_DRIP_OPTIONS = [:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on].freeze
8
+
7
9
  include Enumerable
8
10
 
9
11
  def initialize(dripper)
@@ -42,7 +44,7 @@ module Caffeinate
42
44
 
43
45
  def validate_drip_options(action, options)
44
46
  options.symbolize_keys!
45
- options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on)
47
+ options.assert_valid_keys(*VALID_DRIP_OPTIONS)
46
48
  options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
47
49
  options[:using] ||= @dripper.defaults[:using]
48
50
  options[:step] ||= @dripper.drips.size + 1
@@ -10,14 +10,7 @@ module Caffeinate
10
10
  end
11
11
 
12
12
  module ClassMethods
13
- # Runs the subscriber_block
14
- #
15
- # OrderDripper.subscribe!
16
- def subscribe!
17
- subscribes_block.call
18
- end
19
-
20
- # Returns the campaign's `Caffeinate::CampaignSubscriber`
13
+ # Returns the Campaign's `Caffeinate::CampaignSubscriber`
21
14
  def subscriptions
22
15
  caffeinate_campaign.caffeinate_campaign_subscriptions
23
16
  end
@@ -57,33 +50,6 @@ module Caffeinate
57
50
  def unsubscribe!(subscriber, **args)
58
51
  caffeinate_campaign.unsubscribe!(subscriber, **args)
59
52
  end
60
-
61
- # :nodoc:
62
- def subscribes_block
63
- raise(NotImplementedError, 'Define subscribes') unless @subscribes_block
64
-
65
- @subscribes_block
66
- end
67
-
68
- # The subscriber block. Used to create `::Caffeinate::CampaignSubscribers` subscribers.
69
- #
70
- # subscribes do
71
- # Cart.left_joins(:cart_items)
72
- # .includes(:user)
73
- # .where(completed_at: nil)
74
- # .where(updated_at: 1.day.ago..2.days.ago)
75
- # .having('count(cart_items.id) > 0').each do |cart|
76
- # subscribe(cart, user: cart.user)
77
- # end
78
- # end
79
- #
80
- # No need to worry about checking if the given subscriber being already subscribed.
81
- # The `subscribe` method does that for you.
82
- #
83
- # Optionally, can subscribe a user manually via `Caffeinate::Campaign#subscribe`
84
- def subscribes(&block)
85
- @subscribes_block = block
86
- end
87
53
  end
88
54
  end
89
55
  end
@@ -0,0 +1,70 @@
1
+ module Caffeinate
2
+ class OptionEvaluator
3
+ def initialize(thing, drip, mailing)
4
+ @thing = thing
5
+ @drip = drip
6
+ @mailing = mailing
7
+ end
8
+
9
+ def call
10
+ if @thing.is_a?(Symbol)
11
+ @drip.dripper.new.send(@thing, @drip, @mailing)
12
+ elsif @thing.is_a?(Proc)
13
+ @mailing.instance_exec(&@thing)
14
+ elsif @thing.is_a?(String)
15
+ Time.parse(@thing)
16
+ else
17
+ @thing
18
+ end
19
+ end
20
+ end
21
+
22
+ class ScheduleEvaluator
23
+ def self.call(drip, mailing)
24
+ new(drip, mailing).call
25
+ end
26
+
27
+ attr_reader :mailing
28
+ def initialize(drip, mailing)
29
+ @drip = drip
30
+ @mailing = mailing
31
+ end
32
+
33
+ # todo: test this decision tree.
34
+ def call
35
+ if periodical?
36
+ start = mailing.instance_exec(&options[:start])
37
+ start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
38
+ date = start.from_now
39
+ elsif options[:on]
40
+ date = OptionEvaluator.new(options[:on], self, mailing).call
41
+ else
42
+ date = OptionEvaluator.new(options[:delay], self, mailing).call
43
+ if date.respond_to?(:from_now)
44
+ date = date.from_now
45
+ end
46
+ end
47
+
48
+ if options[:at]
49
+ time = OptionEvaluator.new(options[:at], self, mailing).call
50
+ return date.change(hour: time.hour, min: time.min, sec: time.sec)
51
+ end
52
+
53
+ date
54
+ end
55
+
56
+ def respond_to_missing?(name, include_private = false)
57
+ @drip.respond_to?(name, include_private)
58
+ end
59
+
60
+ def method_missing(method, *args, &block)
61
+ @drip.send(method, *args, &block)
62
+ end
63
+
64
+ private
65
+
66
+ def periodical?
67
+ options[:every].present?
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.14.0'
4
+ VERSION = '2.0.1'
5
5
  end
@@ -17,10 +17,10 @@ Caffeinate.setup do |config|
17
17
  #
18
18
  # Default:
19
19
  # config.async_delivery = false
20
- # config.mailing_job = nil
20
+ # config.async_delivery_class = nil
21
21
  #
22
22
  # config.async_delivery = true
23
- # config.mailing_job = 'MyCustomCaffeinateJob'
23
+ # config.async_delivery_class = 'MyCustomCaffeinateJob'
24
24
  #
25
25
  # == Batching
26
26
  #
@@ -4,7 +4,7 @@ class CreateCaffeinateMailings < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
5
  create_table :caffeinate_mailings do |t|
6
6
  t.references :caffeinate_campaign_subscription, null: false, foreign_key: true, index: { name: 'index_caffeinate_mailings_on_campaign_subscription' }
7
- t.datetime :send_at
7
+ t.datetime :send_at, null: false
8
8
  t.datetime :sent_at
9
9
  t.datetime :skipped_at
10
10
  t.string :mailer_class, null: false
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.14.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Brody
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-18 00:00:00.000000000 Z
11
+ date: 2021-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -122,7 +122,22 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
- description: Ruby on Rails drip campaign engine. Buzzwords!
125
+ - !ruby/object:Gem::Dependency
126
+ name: codecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Create, manage, and send scheduled email sequences and drip campaigns
140
+ from your Rails app.
126
141
  email:
127
142
  - josh@josh.mn
128
143
  executables: []
@@ -169,6 +184,7 @@ files:
169
184
  - lib/caffeinate/engine.rb
170
185
  - lib/caffeinate/helpers.rb
171
186
  - lib/caffeinate/mail_ext.rb
187
+ - lib/caffeinate/schedule_evaluator.rb
172
188
  - lib/caffeinate/url_helpers.rb
173
189
  - lib/caffeinate/version.rb
174
190
  - lib/generators/caffeinate/install_generator.rb
@@ -184,7 +200,7 @@ homepage: https://github.com/joshmn/caffeinate
184
200
  licenses:
185
201
  - MIT
186
202
  metadata: {}
187
- post_install_message:
203
+ post_install_message:
188
204
  rdoc_options: []
189
205
  require_paths:
190
206
  - lib
@@ -199,8 +215,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
199
215
  - !ruby/object:Gem::Version
200
216
  version: '0'
201
217
  requirements: []
202
- rubygems_version: 3.2.0.rc.2
203
- signing_key:
218
+ rubygems_version: 3.1.4
219
+ signing_key:
204
220
  specification_version: 4
205
- summary: Ruby on Rails drip campaign engine. Buzzwords!
221
+ summary: Create, manage, and send scheduled email sequences and drip campaigns from
222
+ your Rails app.
206
223
  test_files: []