caffeinate 0.2.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 +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
|