caffeinate 0.12.0 → 2.0.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 +65 -63
- data/app/models/caffeinate/campaign.rb +6 -3
- data/app/models/caffeinate/campaign_subscription.rb +9 -17
- data/app/models/caffeinate/mailing.rb +2 -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 -46
- data/lib/caffeinate/dripper/callbacks.rb +25 -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/subscriber.rb +1 -35
- data/lib/caffeinate/schedule_evaluator.rb +70 -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 +28 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 267d93d746f23525a65cf5607ef0b4c67195cd9c14260d3f8b9b346cfdeb8ffe
|
4
|
+
data.tar.gz: c3851e78e0ddd35f5735aaa70ce9360cf242ab9ede3e6c6a74700b07c59ad16e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f81b1310248f890a74c37b290d5860f6562763d567ec9fe07ca3aeff27f8aa7a48ff0bb071797f8cef022cdb76ea403a12bdce8f76afae5d1a929c805209cbdf
|
7
|
+
data.tar.gz: efe9b9a8306b3c832d04218b11c10c5772319b38206de5e1c141e1f15a494ab73ec069ea19cddfe2d570edb155d14520f4bdf1525b3eb8a849718bc7e895ab05
|
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
|
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
|
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
|
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
|
-
##
|
73
|
+
## Do this all better in five minutes
|
62
74
|
|
63
|
-
|
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
|
-
###
|
87
|
+
### Clean up the mailer logic
|
87
88
|
|
88
|
-
|
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
|
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
|
-
|
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
|
162
|
+
### Done
|
168
163
|
|
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)`
|
164
|
+
You're done.
|
174
165
|
|
175
|
-
|
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
|
-
|
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?
|
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
|
-
|
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
|
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.
|
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.
|
83
|
+
caffeinate_campaign_subscriptions.find_or_create_by!(subscriber: subscriber, **args)
|
81
84
|
end
|
82
85
|
end
|
83
86
|
end
|
@@ -33,6 +33,9 @@ module Caffeinate
|
|
33
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
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
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
|
38
|
+
|
36
39
|
belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
|
37
40
|
alias_attribute :campaign, :caffeinate_campaign
|
38
41
|
|
@@ -52,7 +55,7 @@ module Caffeinate
|
|
52
55
|
|
53
56
|
before_validation :call_dripper_before_subscribe_blocks!, on: :create
|
54
57
|
|
55
|
-
|
58
|
+
after_create :create_mailings!
|
56
59
|
|
57
60
|
after_commit :on_complete, if: :completed?
|
58
61
|
|
@@ -77,7 +80,7 @@ module Caffeinate
|
|
77
80
|
end
|
78
81
|
|
79
82
|
# Updates `ended_at` and runs `on_complete` callbacks
|
80
|
-
def end!(reason =
|
83
|
+
def end!(reason = ::Caffeinate.config.default_ended_reason)
|
81
84
|
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
82
85
|
|
83
86
|
update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
@@ -87,7 +90,7 @@ module Caffeinate
|
|
87
90
|
end
|
88
91
|
|
89
92
|
# Updates `ended_at` and runs `on_complete` callbacks
|
90
|
-
def end(reason =
|
93
|
+
def end(reason = ::Caffeinate.config.default_ended_reason)
|
91
94
|
return false if unsubscribed?
|
92
95
|
|
93
96
|
result = update(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
@@ -97,7 +100,7 @@ module Caffeinate
|
|
97
100
|
end
|
98
101
|
|
99
102
|
# Updates `unsubscribed_at` and runs `on_subscribe` callbacks
|
100
|
-
def unsubscribe!(reason =
|
103
|
+
def unsubscribe!(reason = ::Caffeinate.config.default_unsubscribe_reason)
|
101
104
|
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended?
|
102
105
|
|
103
106
|
update!(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
|
@@ -107,7 +110,7 @@ module Caffeinate
|
|
107
110
|
end
|
108
111
|
|
109
112
|
# Updates `unsubscribed_at` and runs `on_subscribe` callbacks
|
110
|
-
def unsubscribe(reason =
|
113
|
+
def unsubscribe(reason = ::Caffeinate.config.default_unsubscribe_reason)
|
111
114
|
return false if ended?
|
112
115
|
|
113
116
|
result = update(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
|
@@ -128,18 +131,7 @@ module Caffeinate
|
|
128
131
|
true
|
129
132
|
end
|
130
133
|
|
131
|
-
#
|
132
|
-
# Use `force` to forcefully reset. Does not create the mailings.
|
133
|
-
def resubscribe!(force = false)
|
134
|
-
return false if ended? && !force
|
135
|
-
return false if unsubscribed? && !force
|
136
|
-
|
137
|
-
result = update(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
|
138
|
-
|
139
|
-
caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
|
140
|
-
result
|
141
|
-
end
|
142
|
-
|
134
|
+
# Checks if the record is not new and if mailings are all gone.
|
143
135
|
def completed?
|
144
136
|
caffeinate_mailings.unsent.count.zero?
|
145
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.
|
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)
|
@@ -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,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,36 +23,22 @@ 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
|
+
#
|
31
|
+
# This is kind of messy and could use some love.
|
32
|
+
# todo: better.
|
70
33
|
def enabled?(mailing)
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
74
42
|
end
|
75
43
|
end
|
76
44
|
end
|
@@ -13,12 +13,25 @@ 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")
|
22
35
|
end
|
23
36
|
|
24
37
|
def before_subscribe(&block)
|
@@ -114,10 +127,19 @@ module Caffeinate
|
|
114
127
|
|
115
128
|
# Callback before a Drip has called the mailer.
|
116
129
|
#
|
117
|
-
# before_drip do |
|
130
|
+
# before_drip do |drip, mailing|
|
118
131
|
# Slack.notify(:caffeinate, "#{drip.action_name} is starting")
|
119
132
|
# end
|
120
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
|
+
#
|
121
143
|
# @yield Caffeinate::Drip current drip
|
122
144
|
# @yield Caffeinate::Mailing
|
123
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
|
@@ -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,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
|
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: 2.0.0
|
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-
|
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
|
-
|
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,12 +192,15 @@ 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:
|
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.
|
203
|
-
signing_key:
|
218
|
+
rubygems_version: 3.1.4
|
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: []
|