caffeinate 0.4.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +141 -71
- data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +2 -2
- data/app/models/caffeinate/application_record.rb +0 -1
- data/app/models/caffeinate/campaign.rb +26 -10
- data/app/models/caffeinate/campaign_subscription.rb +40 -10
- data/app/models/caffeinate/mailing.rb +4 -3
- data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +3 -3
- data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -4
- data/lib/caffeinate.rb +3 -8
- data/lib/caffeinate/action_mailer/observer.rb +1 -0
- data/lib/caffeinate/active_record/extension.rb +15 -10
- data/lib/caffeinate/configuration.rb +9 -3
- data/lib/caffeinate/drip.rb +13 -5
- data/lib/caffeinate/drip_evaluator.rb +1 -0
- data/lib/caffeinate/dripper/callbacks.rb +45 -13
- data/lib/caffeinate/dripper/campaign.rb +16 -3
- data/lib/caffeinate/dripper/drip.rb +1 -57
- data/lib/caffeinate/dripper/drip_collection.rb +62 -0
- data/lib/caffeinate/dripper/inferences.rb +3 -1
- data/lib/caffeinate/dripper/perform.rb +11 -21
- data/lib/caffeinate/dripper/periodical.rb +2 -0
- data/lib/caffeinate/dripper/subscriber.rb +14 -2
- data/lib/caffeinate/dripper_collection.rb +17 -0
- data/lib/caffeinate/engine.rb +3 -2
- data/lib/caffeinate/version.rb +1 -1
- data/lib/generators/caffeinate/templates/caffeinate.rb +10 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ebd5c45c4d4fd056ba4ff2080a464e2151bc3245849c709d9f81d396c4b36523
|
4
|
+
data.tar.gz: 0ed91aa4d77c8b368fadc721ffdd81bdd70a249cebe077fadcd2161b6ebf6dab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0dc2f09d245f8936bf55ab0c00d8a4cd4b8651db10f9656a66869088297c43c694b1c71356421216d59a27376fac89962bb8dbd02dfd7d648e65578f2cfb1900
|
7
|
+
data.tar.gz: 3554585afc00fc341c8627ee67071c02ca8f60092f96545043c4c06447a4c6c0ae37e3292cda9293047670471c7b16843dfa3a5b04b4b215ea93562d7050d22b
|
data/README.md
CHANGED
@@ -1,131 +1,201 @@
|
|
1
|
+
<div align="center">
|
2
|
+
<img width="450" src="https://github.com/joshmn/caffeinate/raw/master/logo.png" alt="Caffeinate logo" />
|
3
|
+
</div>
|
4
|
+
|
5
|
+
---
|
6
|
+
|
1
7
|
# Caffeinate
|
2
8
|
|
3
9
|
Caffeinate is a drip campaign engine for Ruby on Rails applications.
|
4
10
|
|
5
|
-
Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
|
11
|
+
Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
|
6
12
|
and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
|
7
13
|
already familiar with: Ruby on Rails.
|
8
14
|
|
9
|
-
|
15
|
+
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).
|
10
16
|
|
11
|
-
##
|
17
|
+
## Do you suffer from ActionMailer tragedies?
|
12
18
|
|
13
|
-
|
19
|
+
If you have _anything_ like this is your codebase, **you need Caffeinate**:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class User < ApplicationRecord
|
23
|
+
after_commit on: :create do
|
24
|
+
OnboardingMailer.welcome_to_my_cool_app(self).deliver_later
|
25
|
+
OnboardingMailer.some_cool_tips(self).deliver_later(wait: 2.days)
|
26
|
+
OnboardingMailer.help_getting_started(self).deliver_later(wait: 3.days)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
14
30
|
|
15
|
-
```ruby
|
16
|
-
class OnboardingMailer < ActionMailer::Base
|
31
|
+
```ruby
|
32
|
+
class OnboardingMailer < ActionMailer::Base
|
17
33
|
# Send on account creation
|
18
34
|
def welcome_to_my_cool_app(user)
|
19
|
-
mail(to: user.email, subject: "
|
35
|
+
mail(to: user.email, subject: "Welcome to CoolApp!")
|
20
36
|
end
|
21
37
|
|
22
38
|
# Send 2 days after the user signs up
|
23
39
|
def some_cool_tips(user)
|
40
|
+
return if user.unsubscribed_from_onboarding_campaign?
|
41
|
+
|
24
42
|
mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
|
25
|
-
end
|
43
|
+
end
|
26
44
|
|
27
45
|
# Sends 3 days after the user signs up and hasn't added a company profile yet
|
28
46
|
def help_getting_started(user)
|
29
|
-
return if user.
|
47
|
+
return if user.unsubscribed_from_onboarding_campaign?
|
48
|
+
return if user.onboarding_completed?
|
30
49
|
|
31
|
-
mail(to: user.email, subject: "
|
32
|
-
end
|
33
|
-
end
|
50
|
+
mail(to: user.email, subject: "Do you need help getting started?")
|
51
|
+
end
|
52
|
+
end
|
34
53
|
```
|
35
54
|
|
36
|
-
|
55
|
+
### What's wrong with this?
|
37
56
|
|
38
|
-
|
57
|
+
* You're checking state in a mailer
|
58
|
+
* The unsubscribe feature is, most likely, tied to a `User`, which means...
|
59
|
+
* It's going to be _so fun_ to scale horizontally
|
39
60
|
|
40
|
-
|
61
|
+
## Caffeinate to the rescue
|
41
62
|
|
42
|
-
|
43
|
-
Caffeinate::Campaign.create!(name: "Onboarding Campaign", slug: "onboarding")
|
44
|
-
```
|
63
|
+
Caffeinate combines a simple scheduling DSL, ActionMailer, and your data models to create scheduled email sequences.
|
45
64
|
|
46
|
-
|
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!
|
47
71
|
|
48
|
-
|
72
|
+
## Onboarding in Caffeinate
|
49
73
|
|
50
|
-
|
51
|
-
class OnboardingDripper < ApplicationDripper
|
52
|
-
drip :welcome_to_my_cool_app, delay: 0.hours
|
53
|
-
drip :some_cool_tips, delay: 2.days
|
54
|
-
drip :help_getting_started, delay: 3.days do
|
55
|
-
if mailing.user.company.present?
|
56
|
-
mailing.unsubscribe!
|
57
|
-
return false
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
```
|
74
|
+
In five minutes you can implement this onboarding campaign, and it won't even hijack your entire app!
|
62
75
|
|
63
|
-
###
|
76
|
+
### Install it
|
64
77
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
78
|
+
Add to Gemfile, run the installer, migrate:
|
79
|
+
|
80
|
+
```bash
|
81
|
+
$ bundle add caffeinate
|
82
|
+
$ rails g caffeinate:install
|
83
|
+
$ rake db:migrate
|
71
84
|
```
|
72
85
|
|
73
|
-
###
|
86
|
+
### Remove that ActionMailer logic
|
87
|
+
|
88
|
+
Just delete it. Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
|
74
89
|
|
75
|
-
|
90
|
+
The only other change you need to make is the argument that the mailer action receives:
|
76
91
|
|
77
92
|
```ruby
|
78
|
-
|
93
|
+
class OnboardingMailer < ActionMailer::Base
|
94
|
+
def welcome_to_my_cool_app(mailing)
|
95
|
+
@user = mailing.subscriber
|
96
|
+
mail(to: @user.email, subject: "Welcome to CoolApp!")
|
97
|
+
end
|
98
|
+
|
99
|
+
def some_cool_tips(mailing)
|
100
|
+
@user = mailing.subscriber
|
101
|
+
mail(to: @user.email, subject: "Here are some cool tips for MyCoolApp")
|
102
|
+
end
|
103
|
+
|
104
|
+
def help_getting_started(mailing)
|
105
|
+
@user = mailing.subscriber
|
106
|
+
mail(to: @user.email, subject: "Do you need help getting started?")
|
107
|
+
end
|
108
|
+
end
|
79
109
|
```
|
80
110
|
|
81
|
-
|
111
|
+
While we're there, let's add an unsubscribe link to the views or layout:
|
82
112
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
* Built on the stack you're already familiar with
|
113
|
+
```erb
|
114
|
+
<%= link_to "Stop receiving onboarding tips :(", caffeinate_unsubscribe_url %>
|
115
|
+
```
|
87
116
|
|
88
|
-
|
117
|
+
### Create a Dripper
|
89
118
|
|
90
|
-
|
119
|
+
A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.
|
91
120
|
|
92
|
-
|
121
|
+
In `app/drippers/onboarding_dripper.rb`:
|
93
122
|
|
94
123
|
```ruby
|
95
|
-
|
124
|
+
class OnboardingDripper < ApplicationDripper
|
125
|
+
drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
|
126
|
+
drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
|
127
|
+
drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
|
128
|
+
end
|
96
129
|
```
|
97
130
|
|
98
|
-
|
131
|
+
The `drip` syntax is `def drip(mailer_action, options = {})`.
|
99
132
|
|
100
|
-
|
101
|
-
$ bundle
|
102
|
-
```
|
133
|
+
### Add a subscriber to the Campaign
|
103
134
|
|
104
|
-
|
135
|
+
Call `OnboardingDripper.subscribe` to subscribe a polymorphic `subscriber` to the Campaign, which creates
|
136
|
+
a `Caffeinate::CampaignSubscription`.
|
105
137
|
|
106
|
-
```
|
107
|
-
|
138
|
+
```ruby
|
139
|
+
class User < ApplicationRecord
|
140
|
+
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
|
150
|
+
end
|
151
|
+
end
|
108
152
|
```
|
109
153
|
|
110
|
-
|
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`.
|
111
157
|
|
112
|
-
|
113
|
-
|
158
|
+
### Run the Dripper
|
159
|
+
|
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
|
+
```ruby
|
164
|
+
OnboardingDripper.perform!
|
114
165
|
```
|
115
166
|
|
167
|
+
### Done. But wait, there's more fun if you want
|
168
|
+
|
169
|
+
* Automatic subscriptions
|
170
|
+
* Campaign-specific unsubscribe links
|
171
|
+
* Reasons for unsubscribing so you can have some sort of analytics
|
172
|
+
* Periodical emails (daily, weekly, monthly digests, anyone?)
|
173
|
+
* Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
|
174
|
+
|
175
|
+
### Done. But wait, there's more fun if you want
|
176
|
+
|
177
|
+
* Automatic subscriptions
|
178
|
+
* Campaign-specific unsubscribe links
|
179
|
+
* Reasons for unsubscribing so you can have some sort of analytics
|
180
|
+
* Periodical emails (daily, weekly, monthly digests, anyone?)
|
181
|
+
* Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
|
182
|
+
|
116
183
|
## Documentation
|
117
184
|
|
118
|
-
* [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
|
119
|
-
* [Better-than-average code documentation](https://rubydoc.info/
|
185
|
+
* [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
|
186
|
+
* [Better-than-average code documentation](https://rubydoc.info/gems/caffeinate)
|
120
187
|
|
121
188
|
## Upcoming features/todo
|
122
189
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
190
|
+
[Handy dandy roadmap](https://github.com/joshmn/caffeinate/projects/1).
|
191
|
+
|
192
|
+
## Alternatives
|
193
|
+
|
194
|
+
Not a fan? There are some alternatives!
|
195
|
+
|
196
|
+
* https://github.com/honeybadger-io/heya
|
197
|
+
* https://github.com/tarr11/dripper
|
198
|
+
* https://github.com/Sology/maily_herald
|
129
199
|
|
130
200
|
## Contributing
|
131
201
|
|
@@ -133,8 +203,8 @@ Just do it.
|
|
133
203
|
|
134
204
|
## Contributors & thanks
|
135
205
|
|
136
|
-
* Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
|
137
|
-
|
206
|
+
* Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
|
207
|
+
|
138
208
|
## License
|
139
209
|
|
140
210
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -13,7 +13,7 @@ module Caffeinate
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def subscribe
|
16
|
-
@campaign_subscription.
|
16
|
+
@campaign_subscription.resubscribe!
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
@@ -22,7 +22,7 @@ module Caffeinate
|
|
22
22
|
Caffeinate::UrlHelpers.caffeinate_subscribe_url(@campaign_subscription, options)
|
23
23
|
end
|
24
24
|
|
25
|
-
def caffeinate_unsubscribe_url
|
25
|
+
def caffeinate_unsubscribe_url(**options)
|
26
26
|
Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(@campaign_subscription, options)
|
27
27
|
end
|
28
28
|
|
@@ -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
|
+
|
17
18
|
has_many :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
|
19
|
+
has_many :subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
|
18
20
|
has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
|
21
|
+
has_many :mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
|
19
22
|
|
20
23
|
# Poorly-named Campaign class resolver
|
21
24
|
def to_dripper
|
22
|
-
Caffeinate.
|
25
|
+
::Caffeinate.dripper_collection.resolve(self)
|
23
26
|
end
|
24
27
|
|
25
28
|
# Convenience method for find_by!(slug: value)
|
@@ -32,10 +35,8 @@ module Caffeinate
|
|
32
35
|
end
|
33
36
|
|
34
37
|
# Checks to see if the subscriber exists.
|
35
|
-
#
|
36
|
-
# Use `find_by` so that we don't have to load the record twice. Often used with `subscribes?`
|
37
38
|
def subscriber(record, **args)
|
38
|
-
|
39
|
+
caffeinate_campaign_subscriptions.find_by(subscriber: record, **args)
|
39
40
|
end
|
40
41
|
|
41
42
|
# Check if the subscriber exists
|
@@ -43,17 +44,32 @@ module Caffeinate
|
|
43
44
|
subscriber(record, **args).present?
|
44
45
|
end
|
45
46
|
|
46
|
-
#
|
47
|
+
# Unsubscribes an object from a campaign.
|
48
|
+
#
|
49
|
+
# Campaign[:onboarding].unsubscribe(Company.first, user: Company.first.admin, reason: "Because I said so")
|
50
|
+
#
|
51
|
+
# is the same as
|
52
|
+
#
|
53
|
+
# Campaign.find_by(slug: "onboarding").caffeinate_campaign_subscriptions.find_by(subscriber: Company.first, user: Company.first.admin).unsubscribe!("Because I said so")
|
54
|
+
#
|
55
|
+
# Just... mintier.
|
56
|
+
def unsubscribe(subscriber, **args)
|
57
|
+
reason = args.delete(:reason)
|
58
|
+
subscription = subscriber(subscriber, **args)
|
59
|
+
raise ActiveRecord::RecordInvalid, subscription if subscription.nil?
|
60
|
+
|
61
|
+
subscription.unsubscribe!(reason)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates a `CampaignSubscription` object for the present Campaign. Allows passing `**args` to
|
65
|
+
# delegate additional arguments to the record. Uses `find_or_create_by`.
|
47
66
|
def subscribe(subscriber, **args)
|
48
67
|
caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
|
49
68
|
end
|
50
69
|
|
51
|
-
# Subscribes an object to a campaign.
|
70
|
+
# Subscribes an object to a campaign. Raises `ActiveRecord::RecordInvalid` if the record was invalid.
|
52
71
|
def subscribe!(subscriber, **args)
|
53
|
-
|
54
|
-
return subscription if subscribe.persisted?
|
55
|
-
|
56
|
-
raise ActiveRecord::RecordInvalid, subscription
|
72
|
+
subscribe(subscriber, **args)
|
57
73
|
end
|
58
74
|
end
|
59
75
|
end
|
@@ -17,23 +17,30 @@
|
|
17
17
|
# updated_at :datetime not null
|
18
18
|
#
|
19
19
|
module Caffeinate
|
20
|
+
# If a record tries to be `unsubscribed!` or `ended!` or `resubscribe!` and it's in a state that is not
|
21
|
+
# correct, raise this
|
22
|
+
class InvalidState < ::ActiveRecord::RecordInvalid; end
|
23
|
+
|
20
24
|
# CampaignSubscription associates an object and its optional user to a Campaign
|
21
25
|
# and its relevant Mailings.
|
22
26
|
class CampaignSubscription < ApplicationRecord
|
23
27
|
self.table_name = 'caffeinate_campaign_subscriptions'
|
24
28
|
|
25
29
|
has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
|
26
|
-
|
30
|
+
has_many :mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
|
31
|
+
|
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, -> { upcoming.unsent.order(send_at: :asc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
34
|
+
|
27
35
|
belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
|
36
|
+
alias_attribute :campaign, :caffeinate_campaign
|
37
|
+
|
28
38
|
belongs_to :subscriber, polymorphic: true
|
29
39
|
belongs_to :user, polymorphic: true, optional: true
|
30
40
|
|
31
41
|
# All CampaignSubscriptions that where `unsubscribed_at` is nil and `ended_at` is nil
|
32
42
|
scope :active, -> { where(unsubscribed_at: nil, ended_at: nil) }
|
33
|
-
|
34
|
-
# All CampaignSubscriptions that where `unsubscribed_at` is nil and `ended_at` is nil
|
35
43
|
scope :subscribed, -> { active }
|
36
|
-
|
37
44
|
scope :unsubscribed, -> { where.not(unsubscribed_at: nil) }
|
38
45
|
|
39
46
|
# All CampaignSubscriptions that where `ended_at` is not nil
|
@@ -44,49 +51,70 @@ module Caffeinate
|
|
44
51
|
|
45
52
|
after_commit :create_mailings!, on: :create
|
46
53
|
|
54
|
+
after_commit :on_complete, if: :completed?
|
55
|
+
|
47
56
|
# Actually deliver and process the mail
|
48
57
|
def deliver!(mailing)
|
49
58
|
caffeinate_campaign.to_dripper.deliver!(mailing)
|
50
59
|
end
|
51
60
|
|
52
|
-
# Checks if the
|
61
|
+
# Checks if the `CampaignSubscription` is not ended and not unsubscribed
|
53
62
|
def subscribed?
|
54
63
|
!ended? && !unsubscribed?
|
55
64
|
end
|
56
65
|
|
57
|
-
# Checks if the CampaignSubscription is not subscribed by checking the presence of `unsubscribed_at`
|
66
|
+
# Checks if the `CampaignSubscription` is not subscribed by checking the presence of `unsubscribed_at`
|
58
67
|
def unsubscribed?
|
59
68
|
unsubscribed_at.present?
|
60
69
|
end
|
61
70
|
|
62
|
-
# Checks if the CampaignSubscription is ended by checking the presence of `ended_at`
|
71
|
+
# Checks if the `CampaignSubscription` is ended by checking the presence of `ended_at`
|
63
72
|
def ended?
|
64
73
|
ended_at.present?
|
65
74
|
end
|
66
75
|
|
67
76
|
# Updates `ended_at` and runs `on_complete` callbacks
|
68
77
|
def end!(reason = nil)
|
78
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
79
|
+
|
69
80
|
update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
70
81
|
|
71
|
-
caffeinate_campaign.to_dripper.run_callbacks(:
|
82
|
+
caffeinate_campaign.to_dripper.run_callbacks(:on_end, self)
|
83
|
+
true
|
72
84
|
end
|
73
85
|
|
74
86
|
# Updates `unsubscribed_at` and runs `on_subscribe` callbacks
|
75
87
|
def unsubscribe!(reason = nil)
|
88
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended?
|
89
|
+
|
76
90
|
update!(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
|
77
91
|
|
78
92
|
caffeinate_campaign.to_dripper.run_callbacks(:on_unsubscribe, self)
|
93
|
+
true
|
79
94
|
end
|
80
95
|
|
81
|
-
# Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks
|
82
|
-
|
96
|
+
# Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks.
|
97
|
+
# Use `force` to forcefully reset. Does not create the mailings.
|
98
|
+
def resubscribe!(force = false)
|
99
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended? && !force
|
100
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed? && !force
|
101
|
+
|
83
102
|
update!(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
|
84
103
|
|
85
104
|
caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
def completed?
|
109
|
+
caffeinate_mailings.unsent.count.zero?
|
86
110
|
end
|
87
111
|
|
88
112
|
private
|
89
113
|
|
114
|
+
def on_complete
|
115
|
+
caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
|
116
|
+
end
|
117
|
+
|
90
118
|
# Create mailings according to the drips registered in the Campaign
|
91
119
|
def create_mailings!
|
92
120
|
caffeinate_campaign.to_dripper.drips.each do |drip|
|
@@ -94,8 +122,10 @@ module Caffeinate
|
|
94
122
|
mailing.save!
|
95
123
|
end
|
96
124
|
caffeinate_campaign.to_dripper.run_callbacks(:on_subscribe, self)
|
125
|
+
true
|
97
126
|
end
|
98
127
|
|
128
|
+
# Sets a unique token
|
99
129
|
def set_token!
|
100
130
|
loop do
|
101
131
|
self.token = SecureRandom.uuid
|
@@ -17,12 +17,12 @@
|
|
17
17
|
module Caffeinate
|
18
18
|
# Records of the mails sent and to be sent for a given `::Caffeinate::CampaignSubscriber`
|
19
19
|
class Mailing < ApplicationRecord
|
20
|
-
CURRENT_THREAD_KEY = :current_caffeinate_mailing
|
21
|
-
|
22
20
|
self.table_name = 'caffeinate_mailings'
|
23
21
|
|
24
22
|
belongs_to :caffeinate_campaign_subscription, class_name: 'Caffeinate::CampaignSubscription'
|
23
|
+
alias_attribute :subscription, :caffeinate_campaign_subscription
|
25
24
|
has_one :caffeinate_campaign, through: :caffeinate_campaign_subscription
|
25
|
+
alias_attribute :campaign, :caffeinate_campaign
|
26
26
|
|
27
27
|
scope :upcoming, -> { unsent.unskipped.where('send_at < ?', ::Caffeinate.config.time_now).order('send_at asc') }
|
28
28
|
scope :unsent, -> { unskipped.where(sent_at: nil) }
|
@@ -70,7 +70,6 @@ module Caffeinate
|
|
70
70
|
end
|
71
71
|
|
72
72
|
# The associated drip
|
73
|
-
# @todo This can be optimized with a better cache
|
74
73
|
def drip
|
75
74
|
@drip ||= caffeinate_campaign.to_dripper.drip_collection.for(mailer_action)
|
76
75
|
end
|
@@ -100,6 +99,8 @@ module Caffeinate
|
|
100
99
|
else
|
101
100
|
deliver!
|
102
101
|
end
|
102
|
+
|
103
|
+
caffeinate_campaign_subscription.touch
|
103
104
|
end
|
104
105
|
|
105
106
|
# Delivers the Mailing in the foreground
|
@@ -7,9 +7,9 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
|
|
7
7
|
create_table :caffeinate_campaign_subscriptions do |t|
|
8
8
|
t.references :caffeinate_campaign, null: false, index: { name: :caffeineate_campaign_subscriptions_on_campaign }, foreign_key: true
|
9
9
|
t.string :subscriber_type, null: false
|
10
|
-
t.
|
10
|
+
t.integer :subscriber_id, null: false
|
11
11
|
t.string :user_type
|
12
|
-
t.
|
12
|
+
t.integer :user_id
|
13
13
|
t.string :token, null: false
|
14
14
|
t.datetime :ended_at
|
15
15
|
t.string :ended_reason
|
@@ -20,6 +20,6 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
|
|
20
20
|
t.timestamps
|
21
21
|
end
|
22
22
|
add_index :caffeinate_campaign_subscriptions, :token, unique: true
|
23
|
-
add_index :caffeinate_campaign_subscriptions, %i[subscriber_id subscriber_type user_id user_type], name: :index_caffeinate_campaign_subscriptions
|
23
|
+
add_index :caffeinate_campaign_subscriptions, %i[caffeinate_campaign_id subscriber_id subscriber_type user_id user_type ended_at resubscribed_at unsubscribed_at], name: :index_caffeinate_campaign_subscriptions
|
24
24
|
end
|
25
25
|
end
|
@@ -14,9 +14,7 @@ class CreateCaffeinateMailings < ActiveRecord::Migration[6.0]
|
|
14
14
|
|
15
15
|
t.timestamps
|
16
16
|
end
|
17
|
-
|
18
|
-
add_index :caffeinate_mailings, :
|
19
|
-
add_index :caffeinate_mailings, :skipped_at
|
20
|
-
add_index :caffeinate_mailings, %i[caffeinate_campaign_subscription_id mailer_class mailer_action], name: :index_caffeinate_mailings
|
17
|
+
|
18
|
+
add_index :caffeinate_mailings, %i[caffeinate_campaign_subscription_id send_at sent_at skipped_at], name: :index_caffeinate_mailings
|
21
19
|
end
|
22
20
|
end
|
data/lib/caffeinate.rb
CHANGED
@@ -8,16 +8,11 @@ require 'caffeinate/url_helpers'
|
|
8
8
|
require 'caffeinate/configuration'
|
9
9
|
require 'caffeinate/dripper/base'
|
10
10
|
require 'caffeinate/deliver_async'
|
11
|
+
require 'caffeinate/dripper_collection'
|
11
12
|
|
12
13
|
module Caffeinate
|
13
|
-
|
14
|
-
|
15
|
-
@dripper_to_campaign_class ||= {}
|
16
|
-
end
|
17
|
-
|
18
|
-
# Convenience method for `dripper_to_campaign_class`
|
19
|
-
def self.register_dripper(name, klass)
|
20
|
-
dripper_to_campaign_class[name.to_sym] = klass
|
14
|
+
def self.dripper_collection
|
15
|
+
@drippers ||= DripperCollection.new
|
21
16
|
end
|
22
17
|
|
23
18
|
# Global configuration
|
@@ -5,30 +5,35 @@ module Caffeinate
|
|
5
5
|
# Includes the ActiveRecord association and relevant scopes for an ActiveRecord-backed model
|
6
6
|
module Extension
|
7
7
|
# Adds the associations for a subscriber
|
8
|
-
def
|
8
|
+
def acts_as_caffeinate_subscriber
|
9
9
|
has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription', dependent: :destroy
|
10
10
|
has_many :caffeinate_campaigns, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Campaign'
|
11
11
|
has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Mailing'
|
12
12
|
|
13
13
|
scope :not_subscribed_to_campaign, lambda { |list|
|
14
|
-
|
15
|
-
|
14
|
+
where.not(id: ::Caffeinate::CampaignSubscription
|
15
|
+
.select(:subscriber_id)
|
16
|
+
.joins(:caffeinate_campaign)
|
17
|
+
.where(subscriber_type: name, caffeinate_campaigns: { slug: list }))
|
16
18
|
}
|
17
19
|
|
18
20
|
scope :unsubscribed_from_campaign, lambda { |list|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
where(id: unsubscribed)
|
21
|
+
where(id: ::Caffeinate::CampaignSubscription
|
22
|
+
.unsubscribed
|
23
|
+
.select(:subscriber_id)
|
24
|
+
.joins(:caffeinate_campaign)
|
25
|
+
.where(subscriber_type: name, caffeinate_campaigns: { slug: list }))
|
25
26
|
}
|
26
27
|
end
|
28
|
+
alias caffeinate_subscriber acts_as_caffeinate_subscriber
|
27
29
|
|
28
30
|
# Adds the associations for a user
|
29
|
-
def
|
31
|
+
def acts_as_caffeinate_user
|
30
32
|
has_many :caffeinate_campaign_subscriptions_as_user, as: :user, class_name: '::Caffeinate::CampaignSubscription'
|
33
|
+
has_many :caffeinate_campaigns_as_user, through: :caffeinate_campaign_subscriptions_as_user, class_name: '::Caffeinate::Campaign'
|
34
|
+
has_many :caffeinate_mailings_as_user, through: :caffeinate_campaign_subscriptions_as_user, class_name: '::Caffeinate::Campaign'
|
31
35
|
end
|
36
|
+
alias caffeinate_user acts_as_caffeinate_user
|
32
37
|
end
|
33
38
|
end
|
34
39
|
end
|
@@ -3,20 +3,26 @@
|
|
3
3
|
module Caffeinate
|
4
4
|
# Global configuration
|
5
5
|
class Configuration
|
6
|
-
attr_accessor :now, :async_delivery, :mailing_job, :batch_size, :drippers_path
|
6
|
+
attr_accessor :now, :async_delivery, :mailing_job, :batch_size, :drippers_path, :implicit_campaigns
|
7
7
|
|
8
8
|
def initialize
|
9
9
|
@now = -> { Time.current }
|
10
10
|
@async_delivery = false
|
11
11
|
@mailing_job = nil
|
12
12
|
@batch_size = 1_000
|
13
|
-
@drippers_path =
|
13
|
+
@drippers_path = 'app/drippers'
|
14
|
+
@implicit_campaigns = true
|
14
15
|
end
|
15
16
|
|
16
17
|
def now=(val)
|
17
18
|
raise ArgumentError, '#now must be a proc' unless val.respond_to?(:call)
|
18
19
|
|
19
|
-
|
20
|
+
@now = val
|
21
|
+
end
|
22
|
+
|
23
|
+
# Automatically create a `::Caffeinate::Campaign` object if not found per `Dripper.inferred_campaign_slug`
|
24
|
+
def implicit_campaigns?
|
25
|
+
@implicit_campaigns == true
|
20
26
|
end
|
21
27
|
|
22
28
|
# The current time, for database calls
|
data/lib/caffeinate/drip.rb
CHANGED
@@ -7,6 +7,7 @@ module Caffeinate
|
|
7
7
|
# Handles the block and provides convenience methods for the drip
|
8
8
|
class Drip
|
9
9
|
attr_reader :dripper, :action, :options, :block
|
10
|
+
|
10
11
|
def initialize(dripper, action, options, &block)
|
11
12
|
@dripper = dripper
|
12
13
|
@action = action
|
@@ -22,13 +23,18 @@ module Caffeinate
|
|
22
23
|
def send_at(mailing = nil)
|
23
24
|
if periodical?
|
24
25
|
start = mailing.instance_exec(&options[:start])
|
25
|
-
if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count
|
26
|
-
|
27
|
-
end
|
28
|
-
start.from_now
|
26
|
+
start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
|
27
|
+
date = start.from_now
|
29
28
|
else
|
30
|
-
options[:delay].from_now
|
29
|
+
date = options[:delay].from_now
|
30
|
+
end
|
31
|
+
|
32
|
+
if options[:at]
|
33
|
+
time = Time.parse(options[:at])
|
34
|
+
return date.change(hour: time.hour, min: time.min, sec: time.sec)
|
31
35
|
end
|
36
|
+
|
37
|
+
date
|
32
38
|
end
|
33
39
|
|
34
40
|
def periodical?
|
@@ -37,6 +43,8 @@ module Caffeinate
|
|
37
43
|
|
38
44
|
# Checks if the drip is enabled
|
39
45
|
def enabled?(mailing)
|
46
|
+
dripper.run_callbacks(:before_drip, self, mailing)
|
47
|
+
|
40
48
|
DripEvaluator.new(mailing).call(&@block)
|
41
49
|
end
|
42
50
|
end
|
@@ -38,20 +38,36 @@ module Caffeinate
|
|
38
38
|
@on_subscribe_blocks ||= []
|
39
39
|
end
|
40
40
|
|
41
|
+
# Callback after a Caffeinate::CampaignSubscription is `#resubscribed!`
|
42
|
+
#
|
43
|
+
# on_resubscribe do |campaign_subscription|
|
44
|
+
# Slack.notify(:caffeinate, "Someone resubscribed to #{campaign_subscription.campaign.name}!")
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @yield Caffeinate::CampaignSubscription
|
48
|
+
def on_resubscribe(&block)
|
49
|
+
on_resubscribe_blocks << block
|
50
|
+
end
|
51
|
+
|
52
|
+
# :nodoc:
|
53
|
+
def on_resubscribe_blocks
|
54
|
+
@on_resubscribe_blocks ||= []
|
55
|
+
end
|
56
|
+
|
41
57
|
# Callback before the mailings get processed.
|
42
58
|
#
|
43
|
-
#
|
59
|
+
# before_perform do |dripper|
|
44
60
|
# Slack.notify(:caffeinate, "Dripper is getting ready for mailing! #{dripper.caffeinate_campaign.name}!")
|
45
61
|
# end
|
46
62
|
#
|
47
63
|
# @yield Caffeinate::Dripper
|
48
|
-
def
|
49
|
-
|
64
|
+
def before_perform(&block)
|
65
|
+
before_perform_blocks << block
|
50
66
|
end
|
51
67
|
|
52
68
|
# :nodoc:
|
53
|
-
def
|
54
|
-
@
|
69
|
+
def before_perform_blocks
|
70
|
+
@before_perform_blocks ||= []
|
55
71
|
end
|
56
72
|
|
57
73
|
# Callback before the mailings get processed in a batch.
|
@@ -62,13 +78,13 @@ module Caffeinate
|
|
62
78
|
#
|
63
79
|
# @yield Caffeinate::Dripper
|
64
80
|
# @yield Caffeinate::Mailing [Array]
|
65
|
-
def
|
66
|
-
|
81
|
+
def on_perform(&block)
|
82
|
+
on_perform_blocks << block
|
67
83
|
end
|
68
84
|
|
69
85
|
# :nodoc:
|
70
|
-
def
|
71
|
-
@
|
86
|
+
def on_perform_blocks
|
87
|
+
@on_perform_blocks ||= []
|
72
88
|
end
|
73
89
|
|
74
90
|
# Callback after the all the mailings have been sent.
|
@@ -79,13 +95,13 @@ module Caffeinate
|
|
79
95
|
#
|
80
96
|
# @yield Caffeinate::Dripper
|
81
97
|
# @yield Caffeinate::Mailing [Array]
|
82
|
-
def
|
83
|
-
|
98
|
+
def after_perform(&block)
|
99
|
+
after_perform_blocks << block
|
84
100
|
end
|
85
101
|
|
86
102
|
# :nodoc:
|
87
|
-
def
|
88
|
-
@
|
103
|
+
def after_perform_blocks
|
104
|
+
@after_perform_blocks ||= []
|
89
105
|
end
|
90
106
|
|
91
107
|
# Callback before a Drip has called the mailer.
|
@@ -171,6 +187,22 @@ module Caffeinate
|
|
171
187
|
@on_unsubscribe_blocks ||= []
|
172
188
|
end
|
173
189
|
|
190
|
+
# Callback after a CampaignSubscriber has ended.
|
191
|
+
#
|
192
|
+
# on_end do |campaign_sub|
|
193
|
+
# Slack.notify(:caffeinate, "#{campaign_sub.id} has ended... sad day.")
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# @yield Caffeinate::CampaignSubscription
|
197
|
+
def on_end(&block)
|
198
|
+
on_end_blocks << block
|
199
|
+
end
|
200
|
+
|
201
|
+
# :nodoc:
|
202
|
+
def on_end_blocks
|
203
|
+
@on_end_blocks ||= []
|
204
|
+
end
|
205
|
+
|
174
206
|
# Callback after a `Caffeinate::Mailing` is skipped.
|
175
207
|
#
|
176
208
|
# on_skip do |campaign_subscription, mailing, message|
|
@@ -31,14 +31,27 @@ module Caffeinate
|
|
31
31
|
def campaign=(slug)
|
32
32
|
@caffeinate_campaign = nil
|
33
33
|
@_campaign_slug = slug.to_sym
|
34
|
-
Caffeinate.
|
34
|
+
Caffeinate.dripper_collection.register(@_campaign_slug, name)
|
35
35
|
end
|
36
36
|
|
37
|
-
# Returns the `Caffeinate::Campaign` object for the Dripper
|
37
|
+
# Returns the `Caffeinate::Campaign` object for the Dripper.
|
38
|
+
#
|
39
|
+
# If `config.implicit_campaigns` is true, this will automatically create a `Caffeinate::Campaign` if one is not
|
40
|
+
# found via the `campaign_slug`.
|
38
41
|
def caffeinate_campaign
|
39
42
|
return @caffeinate_campaign if @caffeinate_campaign.present?
|
40
43
|
|
41
|
-
|
44
|
+
if ::Caffeinate.config.implicit_campaigns?
|
45
|
+
@caffeinate_campaign = ::Caffeinate::Campaign.find_or_initialize_by(slug: campaign_slug)
|
46
|
+
if @caffeinate_campaign.new_record?
|
47
|
+
@caffeinate_campaign.name = "#{name.delete_suffix('Dripper').titleize} Campaign"
|
48
|
+
@caffeinate_campaign.save!
|
49
|
+
end
|
50
|
+
else
|
51
|
+
@caffeinate_campaign = ::Caffeinate::Campaign[campaign_slug]
|
52
|
+
end
|
53
|
+
|
54
|
+
@caffeinate_campaign
|
42
55
|
end
|
43
56
|
alias campaign caffeinate_campaign
|
44
57
|
|
@@ -1,66 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'caffeinate/dripper/drip_collection'
|
3
4
|
module Caffeinate
|
4
5
|
module Dripper
|
5
6
|
# The Drip DSL for registering a drip.
|
6
7
|
module Drip
|
7
|
-
# A collection of Drip objects for a `Caffeinate::Dripper`
|
8
|
-
class DripCollection
|
9
|
-
include Enumerable
|
10
|
-
|
11
|
-
def initialize(dripper)
|
12
|
-
@dripper = dripper
|
13
|
-
@drips = {}
|
14
|
-
end
|
15
|
-
|
16
|
-
def for(action)
|
17
|
-
@drips[action.to_sym]
|
18
|
-
end
|
19
|
-
|
20
|
-
# Register the drip
|
21
|
-
def register(action, options, &block)
|
22
|
-
options = validate_drip_options(action, options)
|
23
|
-
|
24
|
-
@drips[action.to_sym] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
|
25
|
-
end
|
26
|
-
|
27
|
-
def each(&block)
|
28
|
-
@drips.each { |action_name, drip| block.call(action_name, drip) }
|
29
|
-
end
|
30
|
-
|
31
|
-
def values
|
32
|
-
@drips.values
|
33
|
-
end
|
34
|
-
|
35
|
-
def size
|
36
|
-
@drips.size
|
37
|
-
end
|
38
|
-
|
39
|
-
def [](val)
|
40
|
-
@drips[val]
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def validate_drip_options(action, options)
|
46
|
-
options.symbolize_keys!
|
47
|
-
options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer)
|
48
|
-
options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
|
49
|
-
options[:using] ||= @dripper.defaults[:using]
|
50
|
-
options[:step] ||= @dripper.drips.size + 1
|
51
|
-
|
52
|
-
if options[:mailer_class].nil?
|
53
|
-
raise ArgumentError, "You must define :mailer_class or :mailer in the options for #{action.inspect} on #{@dripper.inspect}"
|
54
|
-
end
|
55
|
-
|
56
|
-
if options[:every].nil? && options[:delay].nil?
|
57
|
-
raise ArgumentError, "You must define :delay in the options for #{action.inspect} on #{@dripper.inspect}"
|
58
|
-
end
|
59
|
-
|
60
|
-
options
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
8
|
# :nodoc:
|
65
9
|
def self.included(klass)
|
66
10
|
klass.extend ClassMethods
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Caffeinate
|
4
|
+
module Dripper
|
5
|
+
# A collection of Drip objects for a `Caffeinate::Dripper`
|
6
|
+
class DripCollection
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize(dripper)
|
10
|
+
@dripper = dripper
|
11
|
+
@drips = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def for(action)
|
15
|
+
@drips[action.to_sym]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Register the drip
|
19
|
+
def register(action, options, &block)
|
20
|
+
options = validate_drip_options(action, options)
|
21
|
+
|
22
|
+
@drips[action.to_sym] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def each(&block)
|
26
|
+
@drips.each { |action_name, drip| block.call(action_name, drip) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def values
|
30
|
+
@drips.values
|
31
|
+
end
|
32
|
+
|
33
|
+
def size
|
34
|
+
@drips.size
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](val)
|
38
|
+
@drips[val]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def validate_drip_options(action, options)
|
44
|
+
options.symbolize_keys!
|
45
|
+
options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at)
|
46
|
+
options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
|
47
|
+
options[:using] ||= @dripper.defaults[:using]
|
48
|
+
options[:step] ||= @dripper.drips.size + 1
|
49
|
+
|
50
|
+
if options[:mailer_class].nil?
|
51
|
+
raise ArgumentError, "You must define :mailer_class or :mailer in the options for #{action.inspect} on #{@dripper.inspect}"
|
52
|
+
end
|
53
|
+
|
54
|
+
if options[:every].nil? && options[:delay].nil?
|
55
|
+
raise ArgumentError, "You must define :delay in the options for #{action.inspect} on #{@dripper.inspect}"
|
56
|
+
end
|
57
|
+
|
58
|
+
options
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -15,28 +15,18 @@ module Caffeinate
|
|
15
15
|
#
|
16
16
|
# @return nil
|
17
17
|
def perform!
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
run_callbacks(:before_perform, self)
|
19
|
+
Caffeinate::Mailing
|
20
|
+
.upcoming
|
21
|
+
.unsent
|
22
|
+
.joins(:caffeinate_campaign_subscription)
|
23
|
+
.merge(Caffeinate::CampaignSubscription.active)
|
24
|
+
.in_batches(of: self.class.batch_size)
|
25
|
+
.each do |batch|
|
26
|
+
run_callbacks(:on_perform, self, batch)
|
27
|
+
batch.each(&:process!)
|
24
28
|
end
|
25
|
-
|
26
|
-
run_callbacks(:before_process, self)
|
27
|
-
campaign.caffeinate_campaign_subscriptions
|
28
|
-
.active
|
29
|
-
.joins(:next_caffeinate_mailing)
|
30
|
-
.preload(*preloads)
|
31
|
-
.includes(*includes)
|
32
|
-
.in_batches(of: self.class.batch_size)
|
33
|
-
.each do |batch|
|
34
|
-
run_callbacks(:on_process, self, batch)
|
35
|
-
batch.each do |subscriber|
|
36
|
-
subscriber.next_caffeinate_mailing.process!
|
37
|
-
end
|
38
|
-
end
|
39
|
-
run_callbacks(:after_process, self)
|
29
|
+
run_callbacks(:after_perform, self)
|
40
30
|
nil
|
41
31
|
end
|
42
32
|
|
@@ -18,8 +18,8 @@ module Caffeinate
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Returns the campaign's `Caffeinate::CampaignSubscriber`
|
21
|
-
def
|
22
|
-
caffeinate_campaign.
|
21
|
+
def subscriptions
|
22
|
+
caffeinate_campaign.caffeinate_campaign_subscriptions
|
23
23
|
end
|
24
24
|
|
25
25
|
# Subscribes to the campaign.
|
@@ -34,6 +34,18 @@ module Caffeinate
|
|
34
34
|
caffeinate_campaign.subscribe(subscriber, **args)
|
35
35
|
end
|
36
36
|
|
37
|
+
# Unsubscribes from the campaign.
|
38
|
+
#
|
39
|
+
# OrderDripper.unsubscribe(order, user: order.user)
|
40
|
+
#
|
41
|
+
# @param [ActiveRecord::Base] subscriber The object subscribing
|
42
|
+
# @option [ActiveRecord::Base] :user The associated user (optional)
|
43
|
+
#
|
44
|
+
# @return [Caffeinate::CampaignSubscriber] the CampaignSubscriber
|
45
|
+
def unsubscribe(subscriber, **args)
|
46
|
+
caffeinate_campaign.unsubscribe(subscriber, **args)
|
47
|
+
end
|
48
|
+
|
37
49
|
# :nodoc:
|
38
50
|
def subscribes_block
|
39
51
|
raise(NotImplementedError, 'Define subscribes') unless @subscribes_block
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Caffeinate
|
4
|
+
class DripperCollection
|
5
|
+
def initialize
|
6
|
+
@registry = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def register(name, klass)
|
10
|
+
@registry[name.to_sym] = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def resolve(campaign)
|
14
|
+
@registry[campaign.slug.to_sym].constantize
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/caffeinate/engine.rb
CHANGED
@@ -8,13 +8,14 @@ module Caffeinate
|
|
8
8
|
# Adds Caffeinate to Rails
|
9
9
|
class Engine < ::Rails::Engine
|
10
10
|
isolate_namespace Caffeinate
|
11
|
-
config.eager_load_namespaces << Caffeinate
|
12
11
|
|
12
|
+
# :nocov:
|
13
13
|
config.to_prepare do
|
14
|
-
Dir.glob(Rails.root.join(Caffeinate.config.drippers_path,
|
14
|
+
Dir.glob(Rails.root.join(Caffeinate.config.drippers_path, '**', '*.rb')).sort.each do |dripper|
|
15
15
|
require dripper
|
16
16
|
end
|
17
17
|
end
|
18
|
+
# :nocov:
|
18
19
|
|
19
20
|
ActiveSupport.on_load(:action_mailer) do
|
20
21
|
include ::Caffeinate::ActionMailer::Extension
|
data/lib/caffeinate/version.rb
CHANGED
@@ -31,4 +31,14 @@ Caffeinate.setup do |config|
|
|
31
31
|
# config.batch_size = 1_000
|
32
32
|
#
|
33
33
|
# config.batch_size = 100
|
34
|
+
#
|
35
|
+
# == Implicit Campaigns
|
36
|
+
#
|
37
|
+
# Instead of manually having to create a Campaign, you can let Caffeinate do a `find_or_create_by` at runtime.
|
38
|
+
# This is probably dangerous but it hasn't burned me yet so here you go:
|
39
|
+
#
|
40
|
+
# Default:
|
41
|
+
# config.implicit_campaigns = true
|
42
|
+
#
|
43
|
+
# config.implicit_campaigns = false
|
34
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: caffeinate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Brody
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-12-
|
11
|
+
date: 2020-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: sqlite3
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -149,10 +163,12 @@ files:
|
|
149
163
|
- lib/caffeinate/dripper/defaults.rb
|
150
164
|
- lib/caffeinate/dripper/delivery.rb
|
151
165
|
- lib/caffeinate/dripper/drip.rb
|
166
|
+
- lib/caffeinate/dripper/drip_collection.rb
|
152
167
|
- lib/caffeinate/dripper/inferences.rb
|
153
168
|
- lib/caffeinate/dripper/perform.rb
|
154
169
|
- lib/caffeinate/dripper/periodical.rb
|
155
170
|
- lib/caffeinate/dripper/subscriber.rb
|
171
|
+
- lib/caffeinate/dripper_collection.rb
|
156
172
|
- lib/caffeinate/engine.rb
|
157
173
|
- lib/caffeinate/helpers.rb
|
158
174
|
- lib/caffeinate/mail_ext.rb
|