caffeinate 0.5.0 → 0.6.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 +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
|