caffeinate 0.14.0 → 0.15.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.
- checksums.yaml +4 -4
- data/README.md +60 -63
- data/app/models/caffeinate/campaign.rb +2 -0
- data/app/models/caffeinate/mailing.rb +2 -2
- data/lib/caffeinate/configuration.rb +36 -11
- data/lib/caffeinate/deliver_async.rb +1 -1
- data/lib/caffeinate/drip.rb +3 -43
- data/lib/caffeinate/dripper/drip.rb +16 -1
- data/lib/caffeinate/dripper/drip_collection.rb +3 -1
- data/lib/caffeinate/dripper/subscriber.rb +1 -35
- data/lib/caffeinate/schedule_evaluator.rb +64 -0
- data/lib/caffeinate/version.rb +1 -1
- data/lib/generators/caffeinate/templates/caffeinate.rb +2 -2
- data/lib/generators/caffeinate/templates/migrations/create_caffeinate_mailings.rb.tt +1 -1
- metadata +21 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c8294f4e3189cbc99b5a74d31851c1c752bae5acb417b95793aad434781db65
|
|
4
|
+
data.tar.gz: 94cf500c26c578c950474a6e3716b6e689052880c65e87263c5403c1af17d279
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5861ebed028ffcb28df9565dea9fb3bdd068170631133d5ee743351489820dbe0dd86654c5631f98cd14736ca0ac06210e47de992bbda0ed45345029ba9c9141
|
|
7
|
+
data.tar.gz: ce79dd00459d71dbc987063662ddff59b7bb0b6083e55a2821bbbd72dcbf85283daf26d60c8add3e9492010a30c77608642bce3a20695fcf8784a4399296aaa5
|
data/README.md
CHANGED
|
@@ -2,15 +2,24 @@
|
|
|
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.
|
|
10
19
|
|
|
11
|
-
Caffeinate
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
Caffeinate is a drip email engine for managing, creating, and sending scheduled email sequences from your Ruby on Rails application.
|
|
21
|
+
|
|
22
|
+
Caffeinate provides a simple DSL to create scheduled email sequences which can be used by ActionMailer without any additional configuration.
|
|
14
23
|
|
|
15
24
|
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
25
|
|
|
@@ -30,19 +39,16 @@ end
|
|
|
30
39
|
|
|
31
40
|
```ruby
|
|
32
41
|
class OnboardingMailer < ActionMailer::Base
|
|
33
|
-
# Send on account creation
|
|
34
42
|
def welcome_to_my_cool_app(user)
|
|
35
43
|
mail(to: user.email, subject: "Welcome to CoolApp!")
|
|
36
44
|
end
|
|
37
45
|
|
|
38
|
-
# Send 2 days after the user signs up
|
|
39
46
|
def some_cool_tips(user)
|
|
40
47
|
return if user.unsubscribed_from_onboarding_campaign?
|
|
41
48
|
|
|
42
49
|
mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
|
|
43
50
|
end
|
|
44
51
|
|
|
45
|
-
# Sends 3 days after the user signs up and hasn't added a company profile yet
|
|
46
52
|
def help_getting_started(user)
|
|
47
53
|
return if user.unsubscribed_from_onboarding_campaign?
|
|
48
54
|
return if user.onboarding_completed?
|
|
@@ -56,22 +62,12 @@ end
|
|
|
56
62
|
|
|
57
63
|
* You're checking state in a mailer
|
|
58
64
|
* The unsubscribe feature is, most likely, tied to a `User`, which means...
|
|
59
|
-
* It's going to be _so fun_ to scale
|
|
60
|
-
|
|
61
|
-
## Caffeinate to the rescue
|
|
65
|
+
* It's going to be _so fun_ to scale when you finally want to add more unsubscribe links for different types of sequences
|
|
66
|
+
- "one of your projects has expired", but which one? Then you have to add a column to `projects` and manage all that state... ew
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
## Do this all better in five minutes
|
|
64
69
|
|
|
65
|
-
|
|
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!
|
|
70
|
+
In five minutes you can implement this onboarding campaign:
|
|
75
71
|
|
|
76
72
|
### Install it
|
|
77
73
|
|
|
@@ -83,11 +79,11 @@ $ rails g caffeinate:install
|
|
|
83
79
|
$ rake db:migrate
|
|
84
80
|
```
|
|
85
81
|
|
|
86
|
-
###
|
|
82
|
+
### Clean up the mailer logic
|
|
87
83
|
|
|
88
|
-
|
|
84
|
+
Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
|
|
89
85
|
|
|
90
|
-
The only other change you need to make is the argument that the mailer action receives:
|
|
86
|
+
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
87
|
|
|
92
88
|
```ruby
|
|
93
89
|
class OnboardingMailer < ActionMailer::Base
|
|
@@ -108,27 +104,36 @@ class OnboardingMailer < ActionMailer::Base
|
|
|
108
104
|
end
|
|
109
105
|
```
|
|
110
106
|
|
|
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
107
|
### Create a Dripper
|
|
118
108
|
|
|
119
|
-
A Dripper has all the logic for your
|
|
109
|
+
A Dripper has all the logic for your sequence and coordinates with ActionMailer on what to send.
|
|
120
110
|
|
|
121
111
|
In `app/drippers/onboarding_dripper.rb`:
|
|
122
112
|
|
|
123
113
|
```ruby
|
|
124
114
|
class OnboardingDripper < ApplicationDripper
|
|
115
|
+
# each sequence is a campaign. This will dynamically create one by the given slug
|
|
116
|
+
self.campaign = :onboarding
|
|
117
|
+
|
|
118
|
+
# gets called before every time we process a drip
|
|
119
|
+
before_drip do |_drip, mailing|
|
|
120
|
+
if mailing.subscription.subscriber.onboarding_completed?
|
|
121
|
+
mailing.subscription.unsubscribe!("Completed onboarding")
|
|
122
|
+
throw(:abort)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# map drips to the mailer
|
|
125
127
|
drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
|
|
126
128
|
drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
|
|
127
129
|
drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
|
|
128
130
|
end
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
We want to skip sending the `mailing` if the `subscriber` (`User`) completed onboarding. Let's unsubscribe
|
|
134
|
+
with `#unsubscribe!` and give it an optional reason of `Completed onboarding` so we can reference it later
|
|
135
|
+
when we look at analytics. `throw(:abort)` halts the callback chain just like regular Rails callbacks, stopping the
|
|
136
|
+
mailing from being sent.
|
|
132
137
|
|
|
133
138
|
### Add a subscriber to the Campaign
|
|
134
139
|
|
|
@@ -138,48 +143,37 @@ a `Caffeinate::CampaignSubscription`.
|
|
|
138
143
|
```ruby
|
|
139
144
|
class User < ApplicationRecord
|
|
140
145
|
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
|
|
146
|
+
OnboardingDripper.subscribe!(self)
|
|
150
147
|
end
|
|
151
148
|
end
|
|
152
149
|
```
|
|
153
150
|
|
|
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
151
|
### Run the Dripper
|
|
159
152
|
|
|
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
153
|
```ruby
|
|
164
154
|
OnboardingDripper.perform!
|
|
165
155
|
```
|
|
166
156
|
|
|
167
|
-
### Done
|
|
157
|
+
### Done
|
|
168
158
|
|
|
169
|
-
|
|
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)`
|
|
159
|
+
You're done.
|
|
174
160
|
|
|
175
|
-
|
|
161
|
+
[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,
|
|
162
|
+
tips, tricks, and shortcuts.
|
|
176
163
|
|
|
177
|
-
|
|
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)`
|
|
164
|
+
## But wait, there's more
|
|
182
165
|
|
|
166
|
+
Caffeinate also...
|
|
167
|
+
|
|
168
|
+
* ✅ Allows hyper-precise scheduled times. 9:19AM _in the user's timezone_? Sure! **Only on business days**? YES!
|
|
169
|
+
* ✅ Periodicals
|
|
170
|
+
* ✅ Manages unsubscribes
|
|
171
|
+
* ✅ Works with singular and multiple associations
|
|
172
|
+
* ✅ Compatible with every background processor
|
|
173
|
+
* ✅ Tested against large databases at AngelList and is performant as hell
|
|
174
|
+
* ✅ Effortlessly handles complex workflows
|
|
175
|
+
- Need to skip a certain mailing? You can!
|
|
176
|
+
|
|
183
177
|
## Documentation
|
|
184
178
|
|
|
185
179
|
* [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
|
|
@@ -191,7 +185,7 @@ OnboardingDripper.perform!
|
|
|
191
185
|
|
|
192
186
|
## Alternatives
|
|
193
187
|
|
|
194
|
-
Not a fan?
|
|
188
|
+
Not a fan of Caffeinate? I built it because I wasn't a fan of the alternatives. To each their own:
|
|
195
189
|
|
|
196
190
|
* https://github.com/honeybadger-io/heya
|
|
197
191
|
* https://github.com/tarr11/dripper
|
|
@@ -199,11 +193,14 @@ Not a fan? There are some alternatives!
|
|
|
199
193
|
|
|
200
194
|
## Contributing
|
|
201
195
|
|
|
202
|
-
|
|
196
|
+
There's so much more that can be done with this. I'd love to see what you're thinking.
|
|
197
|
+
|
|
198
|
+
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
199
|
|
|
204
200
|
## Contributors & thanks
|
|
205
201
|
|
|
206
202
|
* Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
|
|
203
|
+
* Thanks to [markokajzer](https://github.com/markokajzer) for listening to me talk about this most mornings.
|
|
207
204
|
|
|
208
205
|
## License
|
|
209
206
|
|
|
@@ -20,6 +20,8 @@ module Caffeinate
|
|
|
20
20
|
has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
|
|
21
21
|
has_many :mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
|
|
22
22
|
|
|
23
|
+
scope :active, -> { where(active: true) }
|
|
24
|
+
|
|
23
25
|
# Poorly-named Campaign class resolver
|
|
24
26
|
def to_dripper
|
|
25
27
|
::Caffeinate.dripper_collection.resolve(self)
|
|
@@ -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.
|
|
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)
|
|
@@ -3,14 +3,44 @@
|
|
|
3
3
|
module Caffeinate
|
|
4
4
|
# Global configuration
|
|
5
5
|
class Configuration
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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.
|
|
13
|
+
# config.async_delivery_class = 'MyWorker'
|
|
14
14
|
module DeliverAsync
|
|
15
15
|
def perform(mailing_id)
|
|
16
16
|
mailing = ::Caffeinate::Mailing.find(mailing_id)
|
data/lib/caffeinate/drip.rb
CHANGED
|
@@ -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
|
-
|
|
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] :
|
|
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(
|
|
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
|
-
#
|
|
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,64 @@
|
|
|
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
|
+
delegate_missing_to :@drip
|
|
24
|
+
|
|
25
|
+
def self.call(drip, mailing)
|
|
26
|
+
new(drip, mailing).call
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :mailing
|
|
30
|
+
def initialize(drip, mailing)
|
|
31
|
+
@drip = drip
|
|
32
|
+
@mailing = mailing
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# todo: test this decision tree.
|
|
36
|
+
def call
|
|
37
|
+
if periodical?
|
|
38
|
+
start = mailing.instance_exec(&options[:start])
|
|
39
|
+
start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
|
|
40
|
+
date = start.from_now
|
|
41
|
+
elsif options[:on]
|
|
42
|
+
date = OptionEvaluator.new(options[:on], self, mailing).call
|
|
43
|
+
else
|
|
44
|
+
date = OptionEvaluator.new(options[:delay], self, mailing).call
|
|
45
|
+
if date.respond_to?(:from_now)
|
|
46
|
+
date = date.from_now
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if options[:at]
|
|
51
|
+
time = OptionEvaluator.new(options[:at], self, mailing).call
|
|
52
|
+
return date.change(hour: time.hour, min: time.min, sec: time.sec)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
date
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def periodical?
|
|
61
|
+
options[:every].present?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/caffeinate/version.rb
CHANGED
|
@@ -17,10 +17,10 @@ Caffeinate.setup do |config|
|
|
|
17
17
|
#
|
|
18
18
|
# Default:
|
|
19
19
|
# config.async_delivery = false
|
|
20
|
-
# config.
|
|
20
|
+
# config.async_delivery_class = nil
|
|
21
21
|
#
|
|
22
22
|
# config.async_delivery = true
|
|
23
|
-
# config.
|
|
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.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh Brody
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-01-
|
|
11
|
+
date: 2021-01-24 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
|
-
|
|
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
|
|
@@ -202,5 +218,6 @@ requirements: []
|
|
|
202
218
|
rubygems_version: 3.2.0.rc.2
|
|
203
219
|
signing_key:
|
|
204
220
|
specification_version: 4
|
|
205
|
-
summary:
|
|
221
|
+
summary: Create, manage, and send scheduled email sequences and drip campaigns from
|
|
222
|
+
your Rails app.
|
|
206
223
|
test_files: []
|