caffeinate 0.9.1 → 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/campaign_subscription.rb +16 -6
- data/app/models/caffeinate/mailing.rb +8 -2
- data/lib/caffeinate/action_mailer/interceptor.rb +0 -1
- data/lib/caffeinate/configuration.rb +43 -9
- data/lib/caffeinate/deliver_async.rb +1 -1
- data/lib/caffeinate/drip.rb +14 -23
- data/lib/caffeinate/drip_evaluator.rb +2 -2
- data/lib/caffeinate/dripper/callbacks.rb +33 -3
- data/lib/caffeinate/dripper/delivery.rb +6 -1
- data/lib/caffeinate/dripper/drip.rb +16 -1
- data/lib/caffeinate/dripper/drip_collection.rb +3 -1
- data/lib/caffeinate/dripper/perform.rb +9 -5
- 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/install_generator.rb +13 -5
- data/lib/generators/caffeinate/templates/caffeinate.rb +13 -2
- data/{db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb → lib/generators/caffeinate/templates/migrations/create_caffeinate_campaign_subscriptions.rb.tt} +1 -1
- data/{db/migrate/20201124183102_create_caffeinate_campaigns.rb → lib/generators/caffeinate/templates/migrations/create_caffeinate_campaigns.rb.tt} +1 -2
- data/{db/migrate/20201124183419_create_caffeinate_mailings.rb → lib/generators/caffeinate/templates/migrations/create_caffeinate_mailings.rb.tt} +2 -4
- metadata +24 -7
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)
|
@@ -28,9 +28,13 @@ module Caffeinate
|
|
28
28
|
|
29
29
|
has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
|
30
30
|
has_many :mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
|
31
|
+
has_many :future_mailings, -> { upcoming.unsent }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
31
32
|
|
32
|
-
has_one :next_caffeinate_mailing, -> { upcoming.unsent.order(send_at: :asc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
33
|
-
has_one :next_mailing, -> { joins(:caffeinate_campaign_subscription).where(
|
33
|
+
has_one :next_caffeinate_mailing, -> { joins(:caffeinate_campaign_subscription).where(caffeinate_campaign_subscriptions: { ended_at: nil, unsubscribed_at: nil }).upcoming.unsent.order(send_at: :asc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
34
|
+
has_one :next_mailing, -> { joins(:caffeinate_campaign_subscription).where(caffeinate_campaign_subscriptions: { ended_at: nil, unsubscribed_at: nil }).upcoming.unsent.order(send_at: :asc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
35
|
+
|
36
|
+
has_one :previous_caffeinate_mailing, -> { sent.order(sent_at: :desc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
37
|
+
has_one :previous_mailing, -> { sent.order(sent_at: :desc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
34
38
|
|
35
39
|
belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
|
36
40
|
alias_attribute :campaign, :caffeinate_campaign
|
@@ -49,6 +53,8 @@ module Caffeinate
|
|
49
53
|
before_validation :set_token!, on: [:create]
|
50
54
|
validates :token, uniqueness: true, on: [:create]
|
51
55
|
|
56
|
+
before_validation :call_dripper_before_subscribe_blocks!, on: :create
|
57
|
+
|
52
58
|
after_commit :create_mailings!, on: :create
|
53
59
|
|
54
60
|
after_commit :on_complete, if: :completed?
|
@@ -74,7 +80,7 @@ module Caffeinate
|
|
74
80
|
end
|
75
81
|
|
76
82
|
# Updates `ended_at` and runs `on_complete` callbacks
|
77
|
-
def end!(reason =
|
83
|
+
def end!(reason = ::Caffeinate.config.default_ended_reason)
|
78
84
|
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
79
85
|
|
80
86
|
update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
@@ -84,7 +90,7 @@ module Caffeinate
|
|
84
90
|
end
|
85
91
|
|
86
92
|
# Updates `ended_at` and runs `on_complete` callbacks
|
87
|
-
def end(reason =
|
93
|
+
def end(reason = ::Caffeinate.config.default_ended_reason)
|
88
94
|
return false if unsubscribed?
|
89
95
|
|
90
96
|
result = update(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
@@ -94,7 +100,7 @@ module Caffeinate
|
|
94
100
|
end
|
95
101
|
|
96
102
|
# Updates `unsubscribed_at` and runs `on_subscribe` callbacks
|
97
|
-
def unsubscribe!(reason =
|
103
|
+
def unsubscribe!(reason = ::Caffeinate.config.default_unsubscribe_reason)
|
98
104
|
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended?
|
99
105
|
|
100
106
|
update!(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
|
@@ -104,7 +110,7 @@ module Caffeinate
|
|
104
110
|
end
|
105
111
|
|
106
112
|
# Updates `unsubscribed_at` and runs `on_subscribe` callbacks
|
107
|
-
def unsubscribe(reason =
|
113
|
+
def unsubscribe(reason = ::Caffeinate.config.default_unsubscribe_reason)
|
108
114
|
return false if ended?
|
109
115
|
|
110
116
|
result = update(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
|
@@ -143,6 +149,10 @@ module Caffeinate
|
|
143
149
|
|
144
150
|
private
|
145
151
|
|
152
|
+
def call_dripper_before_subscribe_blocks!
|
153
|
+
caffeinate_campaign.to_dripper.run_callbacks(:before_subscribe, self)
|
154
|
+
end
|
155
|
+
|
146
156
|
def on_complete
|
147
157
|
caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
|
148
158
|
end
|
@@ -24,12 +24,14 @@ 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) }
|
31
31
|
scope :unskipped, -> { where(skipped_at: nil) }
|
32
32
|
|
33
|
+
after_touch :end_if_no_mailings!
|
34
|
+
|
33
35
|
def initialize_dup(args)
|
34
36
|
super
|
35
37
|
self.send_at = nil
|
@@ -110,7 +112,7 @@ module Caffeinate
|
|
110
112
|
|
111
113
|
# Delivers the Mailing in the background
|
112
114
|
def deliver_later!
|
113
|
-
klass = ::Caffeinate.config.
|
115
|
+
klass = ::Caffeinate.config.async_delivery_class
|
114
116
|
if klass.respond_to?(:perform_later)
|
115
117
|
klass.perform_later(id)
|
116
118
|
elsif klass.respond_to?(:perform_async)
|
@@ -119,5 +121,9 @@ module Caffeinate
|
|
119
121
|
raise NoMethodError, "Neither perform_later or perform_async are defined on #{klass}."
|
120
122
|
end
|
121
123
|
end
|
124
|
+
|
125
|
+
def end_if_no_mailings!
|
126
|
+
end! if future_mailings.empty?
|
127
|
+
end
|
122
128
|
end
|
123
129
|
end
|
@@ -5,7 +5,6 @@ module Caffeinate
|
|
5
5
|
# Handles the evaluation of a drip against a mailing to determine if it ultimately gets delivered.
|
6
6
|
# Also invokes the `before_send` callbacks.
|
7
7
|
class Interceptor
|
8
|
-
# Handles `before_send` callbacks for a `Caffeinate::Dripper`
|
9
8
|
def self.delivering_email(message)
|
10
9
|
mailing = message.caffeinate_mailing
|
11
10
|
return unless mailing
|
@@ -3,15 +3,49 @@
|
|
3
3
|
module Caffeinate
|
4
4
|
# Global configuration
|
5
5
|
class Configuration
|
6
|
-
|
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
|
7
38
|
|
8
39
|
def initialize
|
9
40
|
@now = -> { Time.current }
|
10
41
|
@async_delivery = false
|
11
|
-
@
|
42
|
+
@deliver_later = false
|
43
|
+
@async_delivery_class = nil
|
12
44
|
@batch_size = 1_000
|
13
45
|
@drippers_path = 'app/drippers'
|
14
46
|
@implicit_campaigns = true
|
47
|
+
@default_ended_reason = nil
|
48
|
+
@default_unsubscribe_reason = nil
|
15
49
|
end
|
16
50
|
|
17
51
|
def now=(val)
|
@@ -20,24 +54,24 @@ module Caffeinate
|
|
20
54
|
@now = val
|
21
55
|
end
|
22
56
|
|
23
|
-
# Automatically create a `::Caffeinate::Campaign` object if not found per `Dripper.inferred_campaign_slug`
|
24
57
|
def implicit_campaigns?
|
25
|
-
@implicit_campaigns
|
58
|
+
@implicit_campaigns
|
26
59
|
end
|
27
60
|
|
28
|
-
# The current time, for database calls
|
29
61
|
def time_now
|
30
62
|
@now.call
|
31
63
|
end
|
32
64
|
|
33
|
-
# If delivery is asyncronous
|
34
65
|
def async_delivery?
|
35
66
|
@async_delivery
|
36
67
|
end
|
37
68
|
|
38
|
-
|
39
|
-
|
40
|
-
|
69
|
+
def deliver_later?
|
70
|
+
@deliver_later
|
71
|
+
end
|
72
|
+
|
73
|
+
def async_delivery_class
|
74
|
+
@async_delivery_class.constantize
|
41
75
|
end
|
42
76
|
end
|
43
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,6 +1,8 @@
|
|
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
|
#
|
@@ -21,33 +23,22 @@ module Caffeinate
|
|
21
23
|
end
|
22
24
|
|
23
25
|
def send_at(mailing = nil)
|
24
|
-
|
25
|
-
start = mailing.instance_exec(&options[:start])
|
26
|
-
start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
|
27
|
-
date = start.from_now
|
28
|
-
elsif options[:on]
|
29
|
-
date = mailing.instance_exec(&options[:on])
|
30
|
-
else
|
31
|
-
date = options[:delay].from_now
|
32
|
-
end
|
33
|
-
|
34
|
-
if options[:at]
|
35
|
-
time = Time.parse(options[:at])
|
36
|
-
return date.change(hour: time.hour, min: time.min, sec: time.sec)
|
37
|
-
end
|
38
|
-
|
39
|
-
date
|
40
|
-
end
|
41
|
-
|
42
|
-
def periodical?
|
43
|
-
options[:every].present?
|
26
|
+
::Caffeinate::ScheduleEvaluator.call(self, mailing)
|
44
27
|
end
|
45
28
|
|
46
29
|
# Checks if the drip is enabled
|
30
|
+
#
|
31
|
+
# This is kind of messy and could use some love.
|
32
|
+
# todo: better.
|
47
33
|
def enabled?(mailing)
|
48
|
-
|
49
|
-
|
50
|
-
|
34
|
+
catch(:abort) do
|
35
|
+
if dripper.run_callbacks(:before_drip, self, mailing)
|
36
|
+
return DripEvaluator.new(mailing).call(&@block)
|
37
|
+
else
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
false
|
51
42
|
end
|
52
43
|
end
|
53
44
|
end
|
@@ -13,12 +13,33 @@ module Caffeinate
|
|
13
13
|
self.class.run_callbacks(name, *args)
|
14
14
|
end
|
15
15
|
|
16
|
+
def callbacks_for(name)
|
17
|
+
self.class.callbacks_for(name)
|
18
|
+
end
|
19
|
+
|
16
20
|
module ClassMethods
|
17
21
|
# :nodoc:
|
18
22
|
def run_callbacks(name, *args)
|
19
|
-
|
20
|
-
|
23
|
+
catch(:abort) do
|
24
|
+
callbacks_for(name).each do |callback|
|
25
|
+
callback.call(*args)
|
26
|
+
end
|
27
|
+
return true
|
21
28
|
end
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
# :nodoc:
|
33
|
+
def callbacks_for(name)
|
34
|
+
send("#{name}_blocks")
|
35
|
+
end
|
36
|
+
|
37
|
+
def before_subscribe(&block)
|
38
|
+
before_subscribe_blocks << block
|
39
|
+
end
|
40
|
+
|
41
|
+
def before_subscribe_blocks
|
42
|
+
@before_subscribe_blocks ||= []
|
22
43
|
end
|
23
44
|
|
24
45
|
# Callback after a Caffeinate::CampaignSubscription is created, and after the Caffeinate::Mailings have
|
@@ -106,10 +127,19 @@ module Caffeinate
|
|
106
127
|
|
107
128
|
# Callback before a Drip has called the mailer.
|
108
129
|
#
|
109
|
-
# before_drip do |
|
130
|
+
# before_drip do |drip, mailing|
|
110
131
|
# Slack.notify(:caffeinate, "#{drip.action_name} is starting")
|
111
132
|
# end
|
112
133
|
#
|
134
|
+
# Note: If you want to bail on the mailing for some reason, you need invoke `throw(:abort)`
|
135
|
+
#
|
136
|
+
# before_drip do |drip, mailing|
|
137
|
+
# if mailing.caffeinate_campaign_subscription.subscriber.trial_ended?
|
138
|
+
# unsubscribe!("Trial ended")
|
139
|
+
# throw(:abort)
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
#
|
113
143
|
# @yield Caffeinate::Drip current drip
|
114
144
|
# @yield Caffeinate::Mailing
|
115
145
|
def before_drip(&block)
|
@@ -20,7 +20,12 @@ module Caffeinate
|
|
20
20
|
mailing.mailer_class.constantize.send(mailing.mailer_action, mailing)
|
21
21
|
end
|
22
22
|
message.caffeinate_mailing = mailing
|
23
|
-
|
23
|
+
if ::Caffeinate.config.deliver_later?
|
24
|
+
message.deliver_later
|
25
|
+
else
|
26
|
+
message.deliver
|
27
|
+
end
|
28
|
+
|
24
29
|
end
|
25
30
|
end
|
26
31
|
end
|
@@ -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
|
@@ -16,11 +16,7 @@ module Caffeinate
|
|
16
16
|
# @return nil
|
17
17
|
def perform!
|
18
18
|
run_callbacks(:before_perform, self)
|
19
|
-
|
20
|
-
.upcoming
|
21
|
-
.unsent
|
22
|
-
.joins(:caffeinate_campaign_subscription)
|
23
|
-
.merge(Caffeinate::CampaignSubscription.active.where(caffeinate_campaign: self.campaign))
|
19
|
+
self.class.upcoming_mailings
|
24
20
|
.in_batches(of: self.class.batch_size)
|
25
21
|
.each do |batch|
|
26
22
|
run_callbacks(:on_perform, self, batch)
|
@@ -35,6 +31,14 @@ module Caffeinate
|
|
35
31
|
def perform!
|
36
32
|
new.perform!
|
37
33
|
end
|
34
|
+
|
35
|
+
def upcoming_mailings
|
36
|
+
Caffeinate::Mailing
|
37
|
+
.upcoming
|
38
|
+
.unsent
|
39
|
+
.joins(:caffeinate_campaign_subscription)
|
40
|
+
.merge(Caffeinate::CampaignSubscription.active.where(caffeinate_campaign: campaign))
|
41
|
+
end
|
38
42
|
end
|
39
43
|
end
|
40
44
|
end
|
@@ -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
@@ -5,7 +5,6 @@ module Caffeinate
|
|
5
5
|
# Installs Caffeinate
|
6
6
|
class InstallGenerator < Rails::Generators::Base
|
7
7
|
source_root File.expand_path('templates', __dir__)
|
8
|
-
include ::Rails::Generators::Migration
|
9
8
|
|
10
9
|
desc 'Creates a Caffeinate initializer and copies migrations to your application.'
|
11
10
|
|
@@ -33,12 +32,21 @@ module Caffeinate
|
|
33
32
|
@prev_migration_nr.to_s
|
34
33
|
end
|
35
34
|
|
35
|
+
def migration_version
|
36
|
+
if rails5_and_up?
|
37
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def rails5_and_up?
|
42
|
+
Rails::VERSION::MAJOR >= 5
|
43
|
+
end
|
44
|
+
|
36
45
|
# :nodoc:
|
37
46
|
def copy_migrations
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
Rake::Task['caffeinate:install:migrations'].invoke
|
47
|
+
template 'migrations/create_caffeinate_campaigns.rb', "db/migrate/#{self.class.next_migration_number("")}_create_caffeinate_campaigns.rb"
|
48
|
+
template 'migrations/create_caffeinate_campaign_subscriptions.rb', "db/migrate/#{self.class.next_migration_number("")}_create_caffeinate_campaign_subscriptions.rb"
|
49
|
+
template 'migrations/create_caffeinate_mailings.rb', "db/migrate/#{self.class.next_migration_number("")}_create_caffeinate_mailings.rb"
|
42
50
|
end
|
43
51
|
end
|
44
52
|
end
|
@@ -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
|
#
|
@@ -41,4 +41,15 @@ Caffeinate.setup do |config|
|
|
41
41
|
# config.implicit_campaigns = true
|
42
42
|
#
|
43
43
|
# config.implicit_campaigns = false
|
44
|
+
#
|
45
|
+
# == Default reasons
|
46
|
+
#
|
47
|
+
# The default unsubscribe and end reasons.
|
48
|
+
#
|
49
|
+
# Default:
|
50
|
+
# config.default_unsubscribe_reason = nil
|
51
|
+
# config.default_ended_reason = nil
|
52
|
+
#
|
53
|
+
# config.default_unsubscribe_reason = "User unsubscribed"
|
54
|
+
# config.default_ended_reason = "User ended"
|
44
55
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration
|
3
|
+
class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration<%= migration_version %>
|
4
4
|
def change
|
5
5
|
drop_table :caffeinate_campaign_subscriptions if table_exists?(:caffeinate_campaign_subscriptions)
|
6
6
|
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class CreateCaffeinateCampaigns < ActiveRecord::Migration
|
3
|
+
class CreateCaffeinateCampaigns < ActiveRecord::Migration<%= migration_version %>
|
4
4
|
def change
|
5
|
-
drop_table :caffeinate_campaigns if table_exists?(:caffeinate_campaigns)
|
6
5
|
create_table :caffeinate_campaigns do |t|
|
7
6
|
t.string :name, null: false
|
8
7
|
t.string :slug, null: false
|
@@ -1,12 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class CreateCaffeinateMailings < ActiveRecord::Migration
|
3
|
+
class CreateCaffeinateMailings < ActiveRecord::Migration<%= migration_version %>
|
4
4
|
def change
|
5
|
-
drop_table :caffeinate_mailings if table_exists?(:caffeinate_mailings)
|
6
|
-
|
7
5
|
create_table :caffeinate_mailings do |t|
|
8
6
|
t.references :caffeinate_campaign_subscription, null: false, foreign_key: true, index: { name: 'index_caffeinate_mailings_on_campaign_subscription' }
|
9
|
-
t.datetime :send_at
|
7
|
+
t.datetime :send_at, null: false
|
10
8
|
t.datetime :sent_at
|
11
9
|
t.datetime :skipped_at
|
12
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:
|
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: []
|
@@ -143,9 +158,6 @@ files:
|
|
143
158
|
- app/views/layouts/_caffeinate.html.erb
|
144
159
|
- config/locales/en.yml
|
145
160
|
- config/routes.rb
|
146
|
-
- db/migrate/20201124183102_create_caffeinate_campaigns.rb
|
147
|
-
- db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb
|
148
|
-
- db/migrate/20201124183419_create_caffeinate_mailings.rb
|
149
161
|
- lib/caffeinate.rb
|
150
162
|
- lib/caffeinate/action_mailer.rb
|
151
163
|
- lib/caffeinate/action_mailer/extension.rb
|
@@ -172,6 +184,7 @@ files:
|
|
172
184
|
- lib/caffeinate/engine.rb
|
173
185
|
- lib/caffeinate/helpers.rb
|
174
186
|
- lib/caffeinate/mail_ext.rb
|
187
|
+
- lib/caffeinate/schedule_evaluator.rb
|
175
188
|
- lib/caffeinate/url_helpers.rb
|
176
189
|
- lib/caffeinate/version.rb
|
177
190
|
- lib/generators/caffeinate/install_generator.rb
|
@@ -179,6 +192,9 @@ files:
|
|
179
192
|
- lib/generators/caffeinate/templates/application_dripper.rb
|
180
193
|
- lib/generators/caffeinate/templates/caffeinate.rb
|
181
194
|
- lib/generators/caffeinate/templates/mailer.rb.tt
|
195
|
+
- lib/generators/caffeinate/templates/migrations/create_caffeinate_campaign_subscriptions.rb.tt
|
196
|
+
- lib/generators/caffeinate/templates/migrations/create_caffeinate_campaigns.rb.tt
|
197
|
+
- lib/generators/caffeinate/templates/migrations/create_caffeinate_mailings.rb.tt
|
182
198
|
- lib/generators/caffeinate/views_generator.rb
|
183
199
|
homepage: https://github.com/joshmn/caffeinate
|
184
200
|
licenses:
|
@@ -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: []
|