caffeinate 0.5.0 → 0.6.0
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 +25 -7
- data/app/models/caffeinate/campaign_subscription.rb +25 -12
- 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/active_record/extension.rb +15 -10
- data/lib/caffeinate/configuration.rb +9 -3
- data/lib/caffeinate/drip.rb +4 -3
- data/lib/caffeinate/drip_evaluator.rb +1 -0
- data/lib/caffeinate/dripper/callbacks.rb +16 -1
- 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 +7 -9
- 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 -1
- 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: a3bf57f512b2d4760d99422a5696250ae68e29498fb013a6b7864441c028277f
|
4
|
+
data.tar.gz: 20402615978ccb8fe58a8dba9a229a10e972ea42f03bfad68894744263d2302d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72698ac79d53fe73f71203a31d789916112dac478924e88369de9622195bb9f27d25ced846b44d5896b51d787295591e971c594ede370d0c62ba68f623f8c85b
|
7
|
+
data.tar.gz: 8e9a34fe244c2968f2c808f9a700d4e4502d7441de9b718a647a9dae615b4df36193f8c5e4da248b0722353447ea0d32455bc8a9c0dac99f1aa862958adc0ec6
|
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)
|
@@ -43,17 +46,32 @@ module Caffeinate
|
|
43
46
|
subscriber(record, **args).present?
|
44
47
|
end
|
45
48
|
|
46
|
-
#
|
49
|
+
# Unsubscribes an object from a campaign.
|
50
|
+
#
|
51
|
+
# Campaign[:onboarding].unsubscribe(Company.first, user: Company.first.admin, reason: "Because I said so")
|
52
|
+
#
|
53
|
+
# is the same as
|
54
|
+
#
|
55
|
+
# Campaign.find_by(slug: "onboarding").caffeinate_campaign_subscriptions.find_by(subscriber: Company.first, user: Company.first.admin).unsubscribe!("Because I said so")
|
56
|
+
#
|
57
|
+
# Just... mintier.
|
58
|
+
def unsubscribe(subscriber, **args)
|
59
|
+
reason = args.delete(:reason)
|
60
|
+
subscription = subscriber(subscriber, **args)
|
61
|
+
raise ActiveRecord::RecordInvalid, subscription if subscription.nil?
|
62
|
+
|
63
|
+
subscription.unsubscribe!(reason)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates a `CampaignSubscription` object for the present Campaign. Allows passing `**args` to
|
67
|
+
# delegate additional arguments to the record. Uses `find_or_create_by`.
|
47
68
|
def subscribe(subscriber, **args)
|
48
69
|
caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
|
49
70
|
end
|
50
71
|
|
51
|
-
# Subscribes an object to a campaign.
|
72
|
+
# Subscribes an object to a campaign. Raises `ActiveRecord::RecordInvalid` if the record was invalid.
|
52
73
|
def subscribe!(subscriber, **args)
|
53
|
-
|
54
|
-
return subscription if subscribe.persisted?
|
55
|
-
|
56
|
-
raise ActiveRecord::RecordInvalid, subscription
|
74
|
+
subscribe(subscriber, **args)
|
57
75
|
end
|
58
76
|
end
|
59
77
|
end
|
@@ -24,21 +24,23 @@ module Caffeinate
|
|
24
24
|
# CampaignSubscription associates an object and its optional user to a Campaign
|
25
25
|
# and its relevant Mailings.
|
26
26
|
class CampaignSubscription < ApplicationRecord
|
27
|
-
|
28
27
|
self.table_name = 'caffeinate_campaign_subscriptions'
|
29
28
|
|
30
29
|
has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
|
31
|
-
|
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
|
+
|
32
35
|
belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
|
36
|
+
alias_attribute :campaign, :caffeinate_campaign
|
37
|
+
|
33
38
|
belongs_to :subscriber, polymorphic: true
|
34
39
|
belongs_to :user, polymorphic: true, optional: true
|
35
40
|
|
36
41
|
# All CampaignSubscriptions that where `unsubscribed_at` is nil and `ended_at` is nil
|
37
42
|
scope :active, -> { where(unsubscribed_at: nil, ended_at: nil) }
|
38
|
-
|
39
|
-
# All CampaignSubscriptions that where `unsubscribed_at` is nil and `ended_at` is nil
|
40
43
|
scope :subscribed, -> { active }
|
41
|
-
|
42
44
|
scope :unsubscribed, -> { where.not(unsubscribed_at: nil) }
|
43
45
|
|
44
46
|
# All CampaignSubscriptions that where `ended_at` is not nil
|
@@ -49,29 +51,31 @@ module Caffeinate
|
|
49
51
|
|
50
52
|
after_commit :create_mailings!, on: :create
|
51
53
|
|
54
|
+
after_commit :on_complete, if: :completed?
|
55
|
+
|
52
56
|
# Actually deliver and process the mail
|
53
57
|
def deliver!(mailing)
|
54
58
|
caffeinate_campaign.to_dripper.deliver!(mailing)
|
55
59
|
end
|
56
60
|
|
57
|
-
# Checks if the
|
61
|
+
# Checks if the `CampaignSubscription` is not ended and not unsubscribed
|
58
62
|
def subscribed?
|
59
63
|
!ended? && !unsubscribed?
|
60
64
|
end
|
61
65
|
|
62
|
-
# 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`
|
63
67
|
def unsubscribed?
|
64
68
|
unsubscribed_at.present?
|
65
69
|
end
|
66
70
|
|
67
|
-
# 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`
|
68
72
|
def ended?
|
69
73
|
ended_at.present?
|
70
74
|
end
|
71
75
|
|
72
76
|
# Updates `ended_at` and runs `on_complete` callbacks
|
73
77
|
def end!(reason = nil)
|
74
|
-
raise ::Caffeinate::InvalidState,
|
78
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
75
79
|
|
76
80
|
update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
77
81
|
|
@@ -81,7 +85,7 @@ module Caffeinate
|
|
81
85
|
|
82
86
|
# Updates `unsubscribed_at` and runs `on_subscribe` callbacks
|
83
87
|
def unsubscribe!(reason = nil)
|
84
|
-
raise ::Caffeinate::InvalidState,
|
88
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended?
|
85
89
|
|
86
90
|
update!(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
|
87
91
|
|
@@ -92,8 +96,8 @@ module Caffeinate
|
|
92
96
|
# Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks.
|
93
97
|
# Use `force` to forcefully reset. Does not create the mailings.
|
94
98
|
def resubscribe!(force = false)
|
95
|
-
raise ::Caffeinate::InvalidState,
|
96
|
-
raise ::Caffeinate::InvalidState,
|
99
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended? && !force
|
100
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed? && !force
|
97
101
|
|
98
102
|
update!(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
|
99
103
|
|
@@ -101,8 +105,16 @@ module Caffeinate
|
|
101
105
|
true
|
102
106
|
end
|
103
107
|
|
108
|
+
def completed?
|
109
|
+
caffeinate_mailings.unsent.count.zero?
|
110
|
+
end
|
111
|
+
|
104
112
|
private
|
105
113
|
|
114
|
+
def on_complete
|
115
|
+
caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
|
116
|
+
end
|
117
|
+
|
106
118
|
# Create mailings according to the drips registered in the Campaign
|
107
119
|
def create_mailings!
|
108
120
|
caffeinate_campaign.to_dripper.drips.each do |drip|
|
@@ -113,6 +125,7 @@ module Caffeinate
|
|
113
125
|
true
|
114
126
|
end
|
115
127
|
|
128
|
+
# Sets a unique token
|
116
129
|
def set_token!
|
117
130
|
loop do
|
118
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,9 +23,7 @@ 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
|
-
start += options[:every]
|
27
|
-
end
|
26
|
+
start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
|
28
27
|
start.from_now
|
29
28
|
else
|
30
29
|
options[:delay].from_now
|
@@ -37,6 +36,8 @@ module Caffeinate
|
|
37
36
|
|
38
37
|
# Checks if the drip is enabled
|
39
38
|
def enabled?(mailing)
|
39
|
+
dripper.run_callbacks(:before_drip, self, mailing)
|
40
|
+
|
40
41
|
DripEvaluator.new(mailing).call(&@block)
|
41
42
|
end
|
42
43
|
end
|
@@ -38,6 +38,22 @@ 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|
|
@@ -171,7 +187,6 @@ module Caffeinate
|
|
171
187
|
@on_unsubscribe_blocks ||= []
|
172
188
|
end
|
173
189
|
|
174
|
-
|
175
190
|
# Callback after a CampaignSubscriber has ended.
|
176
191
|
#
|
177
192
|
# on_end do |campaign_sub|
|
@@ -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)
|
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
|
@@ -17,16 +17,14 @@ module Caffeinate
|
|
17
17
|
def perform!
|
18
18
|
run_callbacks(:before_perform, self)
|
19
19
|
Caffeinate::Mailing
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
26
|
run_callbacks(:on_perform, self, batch)
|
27
|
-
batch.each
|
28
|
-
mailing.process!
|
29
|
-
end
|
27
|
+
batch.each(&:process!)
|
30
28
|
end
|
31
29
|
run_callbacks(:after_perform, self)
|
32
30
|
nil
|
@@ -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
@@ -9,11 +9,13 @@ module Caffeinate
|
|
9
9
|
class Engine < ::Rails::Engine
|
10
10
|
isolate_namespace Caffeinate
|
11
11
|
|
12
|
+
# :nocov:
|
12
13
|
config.to_prepare do
|
13
|
-
Dir.glob(Rails.root.join(Caffeinate.config.drippers_path,
|
14
|
+
Dir.glob(Rails.root.join(Caffeinate.config.drippers_path, '**', '*.rb')).sort.each do |dripper|
|
14
15
|
require dripper
|
15
16
|
end
|
16
17
|
end
|
18
|
+
# :nocov:
|
17
19
|
|
18
20
|
ActiveSupport.on_load(:action_mailer) do
|
19
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.6.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: 2020-12-
|
11
|
+
date: 2020-12-11 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
|