caffeinate 0.2.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 +162 -77
- data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +3 -3
- data/app/models/caffeinate/application_record.rb +0 -1
- data/app/models/caffeinate/campaign.rb +49 -2
- data/app/models/caffeinate/campaign_subscription.rb +50 -13
- data/app/models/caffeinate/mailing.rb +14 -6
- data/app/views/layouts/{caffeinate.html.erb → _caffeinate.html.erb} +0 -0
- data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +1 -0
- data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +6 -3
- data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -1
- data/lib/caffeinate.rb +4 -8
- data/lib/caffeinate/action_mailer.rb +4 -4
- data/lib/caffeinate/action_mailer/extension.rb +11 -5
- data/lib/caffeinate/action_mailer/interceptor.rb +4 -2
- data/lib/caffeinate/action_mailer/observer.rb +4 -3
- data/lib/caffeinate/active_record/extension.rb +17 -11
- data/lib/caffeinate/configuration.rb +11 -2
- data/lib/caffeinate/drip.rb +15 -2
- data/lib/caffeinate/drip_evaluator.rb +3 -0
- data/lib/caffeinate/dripper/base.rb +12 -5
- data/lib/caffeinate/dripper/batching.rb +22 -0
- data/lib/caffeinate/dripper/callbacks.rb +89 -6
- data/lib/caffeinate/dripper/campaign.rb +20 -8
- data/lib/caffeinate/dripper/defaults.rb +4 -2
- data/lib/caffeinate/dripper/delivery.rb +8 -8
- data/lib/caffeinate/dripper/drip.rb +3 -42
- data/lib/caffeinate/dripper/drip_collection.rb +62 -0
- data/lib/caffeinate/dripper/inferences.rb +7 -2
- data/lib/caffeinate/dripper/perform.rb +14 -7
- data/lib/caffeinate/dripper/periodical.rb +26 -0
- data/lib/caffeinate/dripper/subscriber.rb +14 -2
- data/lib/caffeinate/dripper_collection.rb +17 -0
- data/lib/caffeinate/engine.rb +6 -4
- data/lib/caffeinate/helpers.rb +3 -0
- data/lib/caffeinate/mail_ext.rb +12 -0
- data/lib/caffeinate/url_helpers.rb +3 -0
- data/lib/caffeinate/version.rb +1 -1
- data/lib/generators/caffeinate/install_generator.rb +5 -1
- data/lib/generators/caffeinate/templates/caffeinate.rb +21 -1
- metadata +22 -4
- data/lib/caffeinate/action_mailer/helpers.rb +0 -12
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,116 +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
|
-
|
11
|
+
Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
|
12
|
+
and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
|
13
|
+
already familiar with: Ruby on Rails.
|
6
14
|
|
7
|
-
[
|
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).
|
8
16
|
|
9
|
-
##
|
17
|
+
## Do you suffer from ActionMailer tragedies?
|
10
18
|
|
11
|
-
|
19
|
+
If you have _anything_ like this is your codebase, **you need Caffeinate**:
|
12
20
|
|
13
|
-
```ruby
|
14
|
-
class
|
15
|
-
|
16
|
-
|
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
|
+
```
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class OnboardingMailer < ActionMailer::Base
|
33
|
+
# Send on account creation
|
34
|
+
def welcome_to_my_cool_app(user)
|
35
|
+
mail(to: user.email, subject: "Welcome to CoolApp!")
|
17
36
|
end
|
18
37
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
38
|
+
# Send 2 days after the user signs up
|
39
|
+
def some_cool_tips(user)
|
40
|
+
return if user.unsubscribed_from_onboarding_campaign?
|
41
|
+
|
42
|
+
mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sends 3 days after the user signs up and hasn't added a company profile yet
|
46
|
+
def help_getting_started(user)
|
47
|
+
return if user.unsubscribed_from_onboarding_campaign?
|
48
|
+
return if user.onboarding_completed?
|
49
|
+
|
50
|
+
mail(to: user.email, subject: "Do you need help getting started?")
|
51
|
+
end
|
52
|
+
end
|
23
53
|
```
|
24
54
|
|
25
|
-
###
|
55
|
+
### What's wrong with this?
|
56
|
+
|
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
|
26
60
|
|
27
|
-
|
28
|
-
|
61
|
+
## Caffeinate to the rescue
|
62
|
+
|
63
|
+
Caffeinate combines a simple scheduling DSL, ActionMailer, and your data models to create scheduled email sequences.
|
64
|
+
|
65
|
+
What can you do with drip campaigns?
|
66
|
+
* Onboard new customers with cool tips and tricks
|
67
|
+
* Remind customers to use your product
|
68
|
+
* Nag customers about using your product
|
69
|
+
* Reach their spam folder after you fail to handle their unsubscribe request
|
70
|
+
* And more!
|
71
|
+
|
72
|
+
## Onboarding in Caffeinate
|
73
|
+
|
74
|
+
In five minutes you can implement this onboarding campaign, and it won't even hijack your entire app!
|
75
|
+
|
76
|
+
### Install it
|
77
|
+
|
78
|
+
Add to Gemfile, run the installer, migrate:
|
79
|
+
|
80
|
+
```bash
|
81
|
+
$ bundle add caffeinate
|
82
|
+
$ rails g caffeinate:install
|
83
|
+
$ rake db:migrate
|
29
84
|
```
|
30
85
|
|
31
|
-
###
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
drip :selling_out_soon, mailer: "AbandonedCartMailer", delay: 8.hours do
|
55
|
-
cart = mailing.subscriber
|
56
|
-
if cart.completed?
|
57
|
-
end! # you can also invoke `unsubscribe!` to cancel this mailing and all future mailings
|
58
|
-
return false
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
86
|
+
### Remove that ActionMailer logic
|
87
|
+
|
88
|
+
Just delete it. Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
|
89
|
+
|
90
|
+
The only other change you need to make is the argument that the mailer action receives:
|
91
|
+
|
92
|
+
```ruby
|
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
|
62
109
|
```
|
63
110
|
|
64
|
-
|
111
|
+
While we're there, let's add an unsubscribe link to the views or layout:
|
65
112
|
|
66
|
-
```
|
67
|
-
|
113
|
+
```erb
|
114
|
+
<%= link_to "Stop receiving onboarding tips :(", caffeinate_unsubscribe_url %>
|
68
115
|
```
|
69
116
|
|
70
|
-
|
117
|
+
### Create a Dripper
|
118
|
+
|
119
|
+
A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.
|
71
120
|
|
72
|
-
|
121
|
+
In `app/drippers/onboarding_dripper.rb`:
|
73
122
|
|
74
|
-
```ruby
|
75
|
-
|
123
|
+
```ruby
|
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
|
76
129
|
```
|
77
130
|
|
78
|
-
|
131
|
+
The `drip` syntax is `def drip(mailer_action, options = {})`.
|
79
132
|
|
80
|
-
|
133
|
+
### Add a subscriber to the Campaign
|
81
134
|
|
82
|
-
|
135
|
+
Call `OnboardingDripper.subscribe` to subscribe a polymorphic `subscriber` to the Campaign, which creates
|
136
|
+
a `Caffeinate::CampaignSubscription`.
|
83
137
|
|
84
138
|
```ruby
|
85
|
-
|
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
|
86
152
|
```
|
87
153
|
|
88
|
-
|
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`.
|
89
157
|
|
90
|
-
|
91
|
-
$ bundle
|
92
|
-
```
|
158
|
+
### Run the Dripper
|
93
159
|
|
94
|
-
|
160
|
+
Running `OnboardingDripper.perform!` every `x` minutes will call `Caffeinate::Mailing#process!` on `Caffeinate::Mailing`
|
161
|
+
records that have `send_at < Time.now`.
|
95
162
|
|
96
|
-
```
|
97
|
-
|
163
|
+
```ruby
|
164
|
+
OnboardingDripper.perform!
|
98
165
|
```
|
99
166
|
|
100
|
-
|
167
|
+
### Done. But wait, there's more fun if you want
|
101
168
|
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
+
|
183
|
+
## Documentation
|
184
|
+
|
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)
|
105
187
|
|
106
188
|
## Upcoming features/todo
|
107
189
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
114
199
|
|
115
200
|
## Contributing
|
116
201
|
|
@@ -118,8 +203,8 @@ Just do it.
|
|
118
203
|
|
119
204
|
## Contributors & thanks
|
120
205
|
|
121
|
-
* Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
|
122
|
-
|
206
|
+
* Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
|
207
|
+
|
123
208
|
## License
|
124
209
|
|
125
210
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
class CampaignSubscriptionsController < ApplicationController
|
5
|
-
layout '
|
5
|
+
layout '_caffeinate'
|
6
6
|
|
7
7
|
helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
|
8
8
|
|
@@ -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,17 +14,64 @@ 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)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Convenience method for find_by!(slug: value)
|
29
|
+
#
|
30
|
+
# ::Caffeinate::Campaign[:onboarding]
|
31
|
+
# # is the same as
|
32
|
+
# ::Caffeinate::Campaign.find_by(slug: :onboarding)
|
33
|
+
def self.[](val)
|
34
|
+
find_by!(slug: val)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Checks to see if the subscriber exists.
|
38
|
+
#
|
39
|
+
# Use `find_by` so that we don't have to load the record twice. Often used with `subscribes?`
|
40
|
+
def subscriber(record, **args)
|
41
|
+
@subscriber ||= caffeinate_campaign_subscriptions.find_by(subscriber: record, **args)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check if the subscriber exists
|
45
|
+
def subscribes?(record, **args)
|
46
|
+
subscriber(record, **args).present?
|
47
|
+
end
|
48
|
+
|
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)
|
23
64
|
end
|
24
65
|
|
25
|
-
#
|
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`.
|
26
68
|
def subscribe(subscriber, **args)
|
27
69
|
caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
|
28
70
|
end
|
71
|
+
|
72
|
+
# Subscribes an object to a campaign. Raises `ActiveRecord::RecordInvalid` if the record was invalid.
|
73
|
+
def subscribe!(subscriber, **args)
|
74
|
+
subscribe(subscriber, **args)
|
75
|
+
end
|
29
76
|
end
|
30
77
|
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
|
-
has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
|
26
|
-
|
29
|
+
has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
|
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,42 +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
|
-
def end!
|
69
|
-
|
77
|
+
def end!(reason = nil)
|
78
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
70
79
|
|
71
|
-
|
80
|
+
update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
81
|
+
|
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
|
-
def unsubscribe!
|
76
|
-
|
87
|
+
def unsubscribe!(reason = nil)
|
88
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended?
|
89
|
+
|
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
|
94
|
+
end
|
95
|
+
|
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
|
+
|
102
|
+
update!(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
|
103
|
+
|
104
|
+
caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
def completed?
|
109
|
+
caffeinate_mailings.unsent.count.zero?
|
79
110
|
end
|
80
111
|
|
81
112
|
private
|
82
113
|
|
114
|
+
def on_complete
|
115
|
+
caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
|
116
|
+
end
|
117
|
+
|
83
118
|
# Create mailings according to the drips registered in the Campaign
|
84
119
|
def create_mailings!
|
85
120
|
caffeinate_campaign.to_dripper.drips.each do |drip|
|
@@ -87,8 +122,10 @@ module Caffeinate
|
|
87
122
|
mailing.save!
|
88
123
|
end
|
89
124
|
caffeinate_campaign.to_dripper.run_callbacks(:on_subscribe, self)
|
125
|
+
true
|
90
126
|
end
|
91
127
|
|
128
|
+
# Sets a unique token
|
92
129
|
def set_token!
|
93
130
|
loop do
|
94
131
|
self.token = SecureRandom.uuid
|