caffeinate 2.1.0 → 2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +78 -5
- data/app/models/caffeinate/campaign.rb +5 -2
- data/app/models/caffeinate/campaign_subscription.rb +20 -1
- data/app/models/caffeinate/mailing.rb +9 -1
- data/app/services/campaign_subscriptions/refuel_service.rb +33 -0
- data/lib/caffeinate/action_proxy.rb +146 -0
- data/lib/caffeinate/configuration.rb +5 -0
- data/lib/caffeinate/drip.rb +43 -0
- data/lib/caffeinate/dripper/callbacks.rb +2 -0
- data/lib/caffeinate/dripper/defaults.rb +1 -1
- data/lib/caffeinate/dripper/drip.rb +28 -1
- data/lib/caffeinate/dripper/drip_collection.rb +3 -25
- data/lib/caffeinate/dripper/periodical.rb +12 -2
- data/lib/caffeinate/dripper_collection.rb +10 -0
- data/lib/caffeinate/message_handler.rb +20 -0
- data/lib/caffeinate/perform.rb +16 -0
- data/lib/caffeinate/periodical_drip.rb +39 -0
- data/lib/caffeinate/rspec/matchers/be_subscribed_to_caffeinate_campaign.rb +44 -0
- data/lib/caffeinate/rspec/matchers/end_caffeinate_campaign_subscription.rb +66 -0
- data/lib/caffeinate/rspec/matchers/subscribe_to_caffeinate_campaign.rb +65 -0
- data/lib/caffeinate/rspec/matchers/unsubscribe_from_caffeinate_campaign.rb +66 -0
- data/lib/caffeinate/rspec/matchers.rb +8 -0
- data/lib/caffeinate/rspec.rb +1 -0
- data/lib/caffeinate/schedule_evaluator.rb +14 -10
- data/lib/caffeinate/version.rb +1 -3
- data/lib/caffeinate.rb +5 -0
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f6b0a68bbf6e8ec71e469224fb7d466833ea4506e40798536b77b6af7bcb128
|
4
|
+
data.tar.gz: 72ebe49b84468147f0455cdcd9986e582a75df9b2b2f7114ef3e76f453a18b94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1d315dab19b59edf7601f43b862648d09867c06218bdb2ff269f49c690087161d195dbd3bbeb465027e46fe1146d4c53a7e820253c11a90f825eba6e027bd3e
|
7
|
+
data.tar.gz: f06087e9f499f8550438113d907741d8c8164637b09a18acc1412135813a8227dde96a57fb23d3febfa499111d6a2daaa337fbfac0d5e9ee0429cb3bbdb03a9f
|
data/README.md
CHANGED
@@ -16,11 +16,15 @@
|
|
16
16
|
|
17
17
|
# Caffeinate
|
18
18
|
|
19
|
-
Caffeinate is a drip
|
19
|
+
Caffeinate is a drip engine for managing, creating, and performing scheduled messages sequences from your Ruby on Rails application. This was originally meant for email, but now supports anything!
|
20
20
|
|
21
|
-
Caffeinate provides a simple DSL to create scheduled
|
21
|
+
Caffeinate provides a simple DSL to create scheduled sequences which can be sent by ActionMailer, or invoked by a Ruby object, without any additional configuration.
|
22
22
|
|
23
|
-
There's a cool demo
|
23
|
+
There's a cool demo app you can spin up [here](https://github.com/joshmn/caffeinate-marketing).
|
24
|
+
|
25
|
+
## Now supports POROs!
|
26
|
+
|
27
|
+
Originally, this was meant for just email, but as of V2.3 supports plain old Ruby objects just as well. Having said, the documentation primarily revolves around using ActionMailer, but it's just as easy to plug in any Ruby class. See `Using Without ActionMailer` below.
|
24
28
|
|
25
29
|
## Is this thing dead?
|
26
30
|
|
@@ -74,6 +78,66 @@ end
|
|
74
78
|
* It's going to be _so fun_ to scale when you finally want to add more unsubscribe links for different types of sequences
|
75
79
|
- "one of your projects has expired", but which one? Then you have to add a column to `projects` and manage all that state... ew
|
76
80
|
|
81
|
+
## Perhaps you suffer from enqueued worker madness
|
82
|
+
|
83
|
+
If you have _anything_ like this is your codebase, **you need Caffeinate**:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class User < ApplicationRecord
|
87
|
+
after_commit on: :create do
|
88
|
+
OnboardingWorker.perform_later(:welcome, self.id)
|
89
|
+
OnboardingWorker.perform_in(2.days, :some_cool_tips, self.id)
|
90
|
+
OnboardingWorker.perform_later(3.days, :help_getting_started, self.id)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class OnboardingWorker
|
97
|
+
include Sidekiq::Worker
|
98
|
+
|
99
|
+
def perform(action, user_id)
|
100
|
+
user = User.find(user_id)
|
101
|
+
user.public_send(action)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class User
|
106
|
+
def welcome
|
107
|
+
send_twilio_message("Welcome to our app!")
|
108
|
+
end
|
109
|
+
|
110
|
+
def some_cool_tips
|
111
|
+
return if self.unsubscribed_from_onboarding_campaign?
|
112
|
+
|
113
|
+
send_twilio_message("Here are some cool tips for MyCoolApp")
|
114
|
+
end
|
115
|
+
|
116
|
+
def help_getting_started
|
117
|
+
return if unsubscribed_from_onboarding_campaign?
|
118
|
+
return if onboarding_completed?
|
119
|
+
|
120
|
+
send_twilio_message("Do you need help getting started?")
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def send_twilio_message(message)
|
126
|
+
twilio_client.messages.create(
|
127
|
+
body: message,
|
128
|
+
to: "+12345678901",
|
129
|
+
from: "+15005550006",
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def twilio_client
|
134
|
+
@twilio_client ||= Twilio::REST::Client.new Rails.application.credentials.twilio[:account_sid], Rails.application.credentials.twilio[:auth_token]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
I don't even need to tell you why this is smelly!
|
140
|
+
|
77
141
|
## Do this all better in five minutes
|
78
142
|
|
79
143
|
In five minutes you can implement this onboarding campaign:
|
@@ -88,9 +152,9 @@ $ rails g caffeinate:install
|
|
88
152
|
$ rake db:migrate
|
89
153
|
```
|
90
154
|
|
91
|
-
### Clean up the
|
155
|
+
### Clean up the business logic
|
92
156
|
|
93
|
-
|
157
|
+
Assuming you intend to use Caffeinate to handle emails using ActionMailer, mailers should be responsible for receiving context and creating a `mail` object. Nothing more. (If you are looking for examples that don't use ActionMailer, see [Without ActionMailer](docs/6-without-action-mailer.md).)
|
94
158
|
|
95
159
|
The only other change you need to make is the argument that the mailer action receives. It will now receive a `Caffeinate::Mailing`. [Learn more about the data models](docs/2-data-models.md):
|
96
160
|
|
@@ -159,10 +223,14 @@ end
|
|
159
223
|
|
160
224
|
### Run the Dripper
|
161
225
|
|
226
|
+
You'll usually do this in a scheduled background job or cron.
|
227
|
+
|
162
228
|
```ruby
|
163
229
|
OnboardingDripper.perform!
|
164
230
|
```
|
165
231
|
|
232
|
+
Alternatively, you can run all of the registered drippers with `Caffeinate.perform!`.
|
233
|
+
|
166
234
|
### Done
|
167
235
|
|
168
236
|
You're done.
|
@@ -170,10 +238,15 @@ You're done.
|
|
170
238
|
[Check out the docs](/docs/README.md) for a more in-depth guide that includes all the options you can use for more complex setups,
|
171
239
|
tips, tricks, and shortcuts.
|
172
240
|
|
241
|
+
## Using Without ActionMailer
|
242
|
+
|
243
|
+
Now supports POROs <sup>that inherit from a magical class</sup>! Using the example above, implementing an SMS client. The same rules apply, just change `mailer_class` or `mailer` to `action_class`, and create a `Caffeinate::ActionProxy` (acts just like an `ActionMailer`). See [Without ActionMailer](docs/6-without-action-mailer.md).) for more.
|
244
|
+
|
173
245
|
## But wait, there's more
|
174
246
|
|
175
247
|
Caffeinate also...
|
176
248
|
|
249
|
+
* ✅ Works with regular Ruby methods as of V2.3
|
177
250
|
* ✅ Allows hyper-precise scheduled times. 9:19AM _in the user's timezone_? Sure! **Only on business days**? YES!
|
178
251
|
* ✅ Periodicals
|
179
252
|
* ✅ Manages unsubscribes
|
@@ -74,13 +74,16 @@ module Caffeinate
|
|
74
74
|
|
75
75
|
# Creates a `CampaignSubscription` object for the present Campaign. Allows passing `**args` to
|
76
76
|
# delegate additional arguments to the record. Uses `find_or_create_by`.
|
77
|
+
#
|
78
|
+
# If a subscription hasn't ended, any existing subscription will be returned.
|
77
79
|
def subscribe(subscriber, **args)
|
78
|
-
caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
|
80
|
+
caffeinate_campaign_subscriptions.active.find_or_create_by(subscriber: subscriber, ended_at: nil, **args)
|
79
81
|
end
|
80
82
|
|
81
83
|
# Subscribes an object to a campaign. Raises `ActiveRecord::RecordInvalid` if the record was invalid.
|
84
|
+
# If a subscription hasn't ended, any existing subscription will be returned.
|
82
85
|
def subscribe!(subscriber, **args)
|
83
|
-
caffeinate_campaign_subscriptions.find_or_create_by!(subscriber: subscriber, **args)
|
86
|
+
caffeinate_campaign_subscriptions.find_or_create_by!(subscriber: subscriber, ended_at: nil, **args)
|
84
87
|
end
|
85
88
|
end
|
86
89
|
end
|
@@ -16,6 +16,7 @@
|
|
16
16
|
# created_at :datetime not null
|
17
17
|
# updated_at :datetime not null
|
18
18
|
#
|
19
|
+
|
19
20
|
module Caffeinate
|
20
21
|
# If a record tries to be `unsubscribed!` or `ended!` or `resubscribe!` and it's in a state that is not
|
21
22
|
# correct, raise this
|
@@ -57,7 +58,23 @@ module Caffeinate
|
|
57
58
|
|
58
59
|
after_create :create_mailings!
|
59
60
|
|
60
|
-
after_commit :on_complete, if: :completed?
|
61
|
+
after_commit :on_complete, if: :completed?, unless: :destroyed?
|
62
|
+
|
63
|
+
# Add (new) drips to a `CampaignSubscriber`.
|
64
|
+
#
|
65
|
+
# Useful if you added new drips to a `Campaign` and have existing `CampaignSubscription`
|
66
|
+
# which you want to add them to.
|
67
|
+
#
|
68
|
+
# Pass `:created_at` if you want to offset `Mailing#send_at` time from the time the `CampaignSubscription`
|
69
|
+
# was originally created. That is to say that if you add a new drip for 5 days from now, the mailing will be sent
|
70
|
+
# 5 days from when the `CampaignSubscription` was created.
|
71
|
+
#
|
72
|
+
# Pass `:current` to offset from the current time (doesn't offset anything, actually)
|
73
|
+
def refuel!(offset: :created_at)
|
74
|
+
::CampaignSubscriptions::RefuelService.new(self, offset: offset).call
|
75
|
+
|
76
|
+
true
|
77
|
+
end
|
61
78
|
|
62
79
|
# Actually deliver and process the mail
|
63
80
|
def deliver!(mailing)
|
@@ -82,6 +99,7 @@ module Caffeinate
|
|
82
99
|
# Updates `ended_at` and runs `on_complete` callbacks
|
83
100
|
def end!(reason = ::Caffeinate.config.default_ended_reason)
|
84
101
|
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
102
|
+
return true if ended?
|
85
103
|
|
86
104
|
update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
87
105
|
|
@@ -92,6 +110,7 @@ module Caffeinate
|
|
92
110
|
# Updates `ended_at` and runs `on_complete` callbacks
|
93
111
|
def end(reason = ::Caffeinate.config.default_ended_reason)
|
94
112
|
return false if unsubscribed?
|
113
|
+
return true if ended?
|
95
114
|
|
96
115
|
result = update(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
|
97
116
|
|
@@ -32,6 +32,14 @@ module Caffeinate
|
|
32
32
|
|
33
33
|
after_touch :end_if_no_mailings!
|
34
34
|
|
35
|
+
def self.find_or_initialize_from_drip(campaign_subscription, drip)
|
36
|
+
find_or_initialize_by(
|
37
|
+
caffeinate_campaign_subscription: campaign_subscription,
|
38
|
+
mailer_class: drip.options[:mailer_class],
|
39
|
+
mailer_action: drip.action
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
35
43
|
def initialize_dup(args)
|
36
44
|
super
|
37
45
|
self.send_at = nil
|
@@ -89,7 +97,7 @@ module Caffeinate
|
|
89
97
|
# Assigns attributes to the Mailing from the Drip
|
90
98
|
def from_drip(drip)
|
91
99
|
self.send_at = drip.send_at(self)
|
92
|
-
self.mailer_class = drip.options[:mailer_class]
|
100
|
+
self.mailer_class = drip.options[:mailer_class] || drip.options[:action_class]
|
93
101
|
self.mailer_action = drip.action
|
94
102
|
self
|
95
103
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CampaignSubscriptions
|
2
|
+
class RefuelService
|
3
|
+
|
4
|
+
def initialize(campaign_subscription, offset: :created_at)
|
5
|
+
raise ArgumentError, "must be either :current or :created_at" unless [:created_at, :current].include?(offset.to_sym)
|
6
|
+
|
7
|
+
@campaign_subscription = campaign_subscription
|
8
|
+
@campaign = @campaign_subscription.caffeinate_campaign
|
9
|
+
@offset = offset.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
mailings = []
|
14
|
+
|
15
|
+
@campaign.to_dripper.drips.each do |drip|
|
16
|
+
mailing = Caffeinate::Mailing.find_or_initialize_from_drip(@campaign_subscription, drip)
|
17
|
+
if mailing.new_record?
|
18
|
+
mailing.send_at = drip.send_at(@campaign_subscription)
|
19
|
+
if @offset == :created_at
|
20
|
+
mailing.send_at + (Caffeinate.config.now.call - @campaign_subscription.created_at)
|
21
|
+
elsif @offset == :current
|
22
|
+
# do nothing on purpose!
|
23
|
+
end
|
24
|
+
|
25
|
+
mailing.save!
|
26
|
+
mailings << mailing
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
mailings
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'caffeinate/message_handler'
|
2
|
+
|
3
|
+
module Caffeinate
|
4
|
+
# Allows you to use a PORO for a drip; acts just like ActionMailer::Base
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# class TextAction < Caffeinate::ActionProxy
|
8
|
+
# def welcome(mailing)
|
9
|
+
# user = mailing.subscriber
|
10
|
+
# HTTParty.post("...") # ...
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# In the future (when?), "mailing" objects will become "messages".
|
15
|
+
#
|
16
|
+
# Optionally, you can use the method for setup and return an object that implements `#deliver!`
|
17
|
+
# and that will be invoked.
|
18
|
+
#
|
19
|
+
# usage:
|
20
|
+
#
|
21
|
+
# class TextAction < Caffeinate::ActionProxy
|
22
|
+
# class Envelope(user)
|
23
|
+
# @sms = SMS.new(to: user.phone_number)
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def deliver!(action)
|
27
|
+
# # action will be the instantiated TextAction object
|
28
|
+
# # and you can access action.action_name, etc.
|
29
|
+
#
|
30
|
+
# erb = ERB.new(File.read(Rails.root + "app/views/cool_one_off_action/#{action_object.action_name}.text.erb"))
|
31
|
+
# # ...
|
32
|
+
# @sms.send!
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def welcome(mailing)
|
36
|
+
# Envelope.new(mailing.subscriber)
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
class ActionProxy
|
40
|
+
attr_accessor :caffeinate_mailing
|
41
|
+
attr_accessor :perform_deliveries
|
42
|
+
attr_reader :action_name
|
43
|
+
|
44
|
+
class DeliveryMethod
|
45
|
+
def deliver!(action)
|
46
|
+
# implement this if you want to
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@delivery_method = DeliveryMethod.new
|
52
|
+
@perform_deliveries = true # will only be false if interceptors set it so
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def action_methods
|
57
|
+
@action_methods ||= begin
|
58
|
+
methods = (public_instance_methods(true) -
|
59
|
+
internal_methods +
|
60
|
+
public_instance_methods(false))
|
61
|
+
methods.map!(&:to_s)
|
62
|
+
methods.to_set
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def internal_methods
|
67
|
+
controller = self
|
68
|
+
|
69
|
+
controller = controller.superclass until controller.abstract?
|
70
|
+
controller.public_instance_methods(true)
|
71
|
+
end
|
72
|
+
|
73
|
+
def method_missing(method_name, *args)
|
74
|
+
if action_methods.include?(method_name.to_s)
|
75
|
+
::Caffeinate::MessageHandler.new(self, method_name, *args)
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
ruby2_keywords(:method_missing)
|
81
|
+
|
82
|
+
def respond_to_missing?(method, include_all = false)
|
83
|
+
action_methods.include?(method.to_s) || super
|
84
|
+
end
|
85
|
+
|
86
|
+
def abstract?
|
87
|
+
true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def process(action_name, action_args)
|
92
|
+
@action_name = action_name # pass-through for #send
|
93
|
+
@action_args = action_args # pass-through for #send
|
94
|
+
self.caffeinate_mailing = action_args if action_args.is_a?(Caffeinate::Mailing)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Follows Mail::Message
|
98
|
+
def deliver
|
99
|
+
inform_interceptors
|
100
|
+
do_delivery
|
101
|
+
inform_observers
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
# This method bypasses checking perform_deliveries and raise_delivery_errors,
|
106
|
+
# so use with caution.
|
107
|
+
#
|
108
|
+
# It still however fires off the interceptors and calls the observers callbacks if they are defined.
|
109
|
+
#
|
110
|
+
# Returns self
|
111
|
+
def deliver!
|
112
|
+
inform_interceptors
|
113
|
+
handled = send(@action_name, @action_args)
|
114
|
+
if handled.respond_to?(:deliver!) && !handled.is_a?(Caffeinate::Mailing)
|
115
|
+
handled.deliver!(self)
|
116
|
+
end
|
117
|
+
inform_observers
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def inform_interceptors
|
124
|
+
::Caffeinate::ActionMailer::Interceptor.delivering_email(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
def inform_observers
|
128
|
+
::Caffeinate::ActionMailer::Observer.delivered_email(self)
|
129
|
+
end
|
130
|
+
|
131
|
+
# In your action's method (@action_name), if you return an object that responds to `deliver!`
|
132
|
+
# we'll invoke it. This is useful for doing setup in the method and then firing it later.
|
133
|
+
def do_delivery
|
134
|
+
begin
|
135
|
+
if perform_deliveries
|
136
|
+
handled = send(@action_name, @action_args)
|
137
|
+
if handled.respond_to?(:deliver!) && !handled.is_a?(Caffeinate::Mailing)
|
138
|
+
handled.deliver!(self)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
rescue => e
|
142
|
+
raise e
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -36,6 +36,10 @@ module Caffeinate
|
|
36
36
|
# The default reason for an unsubscribed `Caffeinate::CampaignSubscription`
|
37
37
|
attr_accessor :default_unsubscribe_reason
|
38
38
|
|
39
|
+
# An array of Drippers that are enabled. Only used if you use Caffeinate.perform in
|
40
|
+
# your worker instead of calling separate drippers. If nil, will run all the campaigns.
|
41
|
+
attr_accessor :enabled_drippers
|
42
|
+
|
39
43
|
def initialize
|
40
44
|
@now = -> { Time.current }
|
41
45
|
@async_delivery = false
|
@@ -46,6 +50,7 @@ module Caffeinate
|
|
46
50
|
@implicit_campaigns = true
|
47
51
|
@default_ended_reason = nil
|
48
52
|
@default_unsubscribe_reason = nil
|
53
|
+
@enabled_drippers = nil
|
49
54
|
end
|
50
55
|
|
51
56
|
def now=(val)
|
data/lib/caffeinate/drip.rb
CHANGED
@@ -8,6 +8,42 @@ module Caffeinate
|
|
8
8
|
#
|
9
9
|
# Handles the block and provides convenience methods for the drip
|
10
10
|
class Drip
|
11
|
+
ALL_DRIP_OPTIONS = [:mailer_class, :mailer, :start, :using, :step]
|
12
|
+
VALID_DRIP_OPTIONS = ALL_DRIP_OPTIONS + [:delay, :start, :at, :on].freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def build(dripper, action, options, &block)
|
16
|
+
options = options.with_defaults(dripper.defaults)
|
17
|
+
validate_drip_options(dripper, action, options)
|
18
|
+
|
19
|
+
new(dripper, action, options, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def validate_drip_options(dripper, action, options)
|
25
|
+
options = normalize_options(dripper, options)
|
26
|
+
|
27
|
+
if options[:mailer_class].nil? && options[:action_class].nil?
|
28
|
+
raise ArgumentError, "You must define :mailer_class, :mailer, or :action_class in the options for #{action.inspect} on #{dripper.inspect}"
|
29
|
+
end
|
30
|
+
|
31
|
+
if options[:every].nil? && options[:delay].nil? && options[:on].nil?
|
32
|
+
raise ArgumentError, "You must define :delay or :on or :every in the options for #{action.inspect} on #{dripper.inspect}"
|
33
|
+
end
|
34
|
+
|
35
|
+
options
|
36
|
+
end
|
37
|
+
|
38
|
+
def normalize_options(dripper, options)
|
39
|
+
options[:mailer_class] ||= options[:mailer] || dripper.defaults[:mailer_class]
|
40
|
+
options[:using] ||= dripper.defaults[:using]
|
41
|
+
options[:step] ||= dripper.drips.size + 1
|
42
|
+
|
43
|
+
options
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
11
47
|
attr_reader :dripper, :action, :options, :block
|
12
48
|
|
13
49
|
def initialize(dripper, action, options, &block)
|
@@ -40,5 +76,12 @@ module Caffeinate
|
|
40
76
|
end
|
41
77
|
false
|
42
78
|
end
|
79
|
+
|
80
|
+
# allows for hitting type.periodical? or type.drip?
|
81
|
+
def type
|
82
|
+
name = self.class.name.demodulize.delete_suffix("Drip").presence || "Drip"
|
83
|
+
|
84
|
+
ActiveSupport::StringInquirer.new(name.downcase)
|
85
|
+
end
|
43
86
|
end
|
44
87
|
end
|
@@ -25,7 +25,7 @@ module Caffeinate
|
|
25
25
|
# @option options [String] :mailer_class The mailer class
|
26
26
|
def default(options = {})
|
27
27
|
options.symbolize_keys!
|
28
|
-
options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
|
28
|
+
options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size, :action_class)
|
29
29
|
@defaults = options
|
30
30
|
end
|
31
31
|
end
|
@@ -47,7 +47,34 @@ module Caffeinate
|
|
47
47
|
#
|
48
48
|
# @option options [Symbol] :using Set to `:parameters` if the mailer action uses ActionMailer::Parameters
|
49
49
|
def drip(action_name, options = {}, &block)
|
50
|
-
drip_collection.register(action_name, options, &block)
|
50
|
+
drip_collection.register(action_name, options, ::Caffeinate::Drip, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Register a Periodical drip on the Dripper
|
54
|
+
#
|
55
|
+
# periodical :pay_your_invoice, every: 1.day, start: 0.hours, if: :invoice_unpaid?
|
56
|
+
#
|
57
|
+
# @param action_name [Symbol] the name of the mailer action
|
58
|
+
# @param [Hash] options the options to create a drip with
|
59
|
+
# @option options [String] :mailer_class The mailer_class
|
60
|
+
# @option options [Symbol|Proc|ActiveSupport::Duration] :every How often the mailing should be created
|
61
|
+
# @option options [Symbol|Proc] :if If the periodical should create another mailing
|
62
|
+
# @option options [Symbol|Proc] :start The offset time to start the clock (only used on the first mailing creation)
|
63
|
+
#
|
64
|
+
# periodical :pay_your_invoice, mailer_class: "InvoiceReminderMailer", if: :invoice_unpaid?
|
65
|
+
#
|
66
|
+
# class MyDripper
|
67
|
+
# drip :mailer_action_name, mailer_class: "MailerClass", at: :generate_date
|
68
|
+
# def generate_date(drip, mailing)
|
69
|
+
# 3.days.from_now.in_time_zone(mailing.subscriber.timezone)
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# drip :mailer_action_name, mailer_class: "MailerClass", at: 'January 1, 2022'
|
74
|
+
#
|
75
|
+
# @option options [Symbol] :using Set to `:parameters` if the mailer action uses ActionMailer::Parameters
|
76
|
+
def periodical_drip(action_name, options = {}, &block)
|
77
|
+
drip_collection.register(action_name, options, ::Caffeinate::PeriodicalDrip, &block)
|
51
78
|
end
|
52
79
|
end
|
53
80
|
end
|
@@ -4,7 +4,7 @@ module Caffeinate
|
|
4
4
|
module Dripper
|
5
5
|
# A collection of Drip objects for a `Caffeinate::Dripper`
|
6
6
|
class DripCollection
|
7
|
-
VALID_DRIP_OPTIONS = [:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on].freeze
|
7
|
+
VALID_DRIP_OPTIONS = [:mailer_class, :action_class, :step, :delay, :every, :start, :using, :mailer, :at, :on].freeze
|
8
8
|
|
9
9
|
include Enumerable
|
10
10
|
|
@@ -18,10 +18,8 @@ module Caffeinate
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Register the drip
|
21
|
-
def register(action, options, &block)
|
22
|
-
|
23
|
-
|
24
|
-
@drips[action.to_sym] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
|
21
|
+
def register(action, options, type = ::Caffeinate::Drip, &block)
|
22
|
+
@drips[action.to_sym] = type.build(@dripper, action, options, &block)
|
25
23
|
end
|
26
24
|
|
27
25
|
def each(&block)
|
@@ -39,26 +37,6 @@ module Caffeinate
|
|
39
37
|
def [](val)
|
40
38
|
@drips[val]
|
41
39
|
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def validate_drip_options(action, options)
|
46
|
-
options.symbolize_keys!
|
47
|
-
options.assert_valid_keys(*VALID_DRIP_OPTIONS)
|
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? && options[:on].nil?
|
57
|
-
raise ArgumentError, "You must define :delay or :on or :every in the options for #{action.inspect} on #{@dripper.inspect}"
|
58
|
-
end
|
59
|
-
|
60
|
-
options
|
61
|
-
end
|
62
40
|
end
|
63
41
|
end
|
64
42
|
end
|
@@ -11,12 +11,22 @@ module Caffeinate
|
|
11
11
|
def periodical(action_name, every:, start: -> { ::Caffeinate.config.time_now }, **options, &block)
|
12
12
|
options[:start] = start
|
13
13
|
options[:every] = every
|
14
|
-
|
14
|
+
periodical_drip(action_name, **options, &block)
|
15
|
+
|
15
16
|
after_send do |mailing, _message|
|
16
|
-
|
17
|
+
make_email = -> {
|
17
18
|
next_mailing = mailing.dup
|
18
19
|
next_mailing.send_at = mailing.drip.send_at(mailing)
|
19
20
|
next_mailing.save!
|
21
|
+
}
|
22
|
+
if mailing.drip.action == action_name
|
23
|
+
if condition = mailing.drip.options[:if]
|
24
|
+
if OptionEvaluator.new(condition, mailing.drip, mailing).call
|
25
|
+
make_email.call
|
26
|
+
end
|
27
|
+
else
|
28
|
+
make_email.call
|
29
|
+
end
|
20
30
|
end
|
21
31
|
end
|
22
32
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Caffeinate
|
4
4
|
class DripperCollection
|
5
|
+
delegate :each, to: :@registry
|
6
|
+
|
5
7
|
def initialize
|
6
8
|
@registry = {}
|
7
9
|
end
|
@@ -13,5 +15,13 @@ module Caffeinate
|
|
13
15
|
def resolve(campaign)
|
14
16
|
@registry[campaign.slug.to_sym].constantize
|
15
17
|
end
|
18
|
+
|
19
|
+
def drippers
|
20
|
+
@registry.values
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear!
|
24
|
+
@registry = {}
|
25
|
+
end
|
16
26
|
end
|
17
27
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
# Delegates methods to a Caffeinate::Action class
|
3
|
+
class MessageHandler < Delegator
|
4
|
+
def initialize(action_class, action, message) # :nodoc:
|
5
|
+
@action_class, @action, @message = action_class, action, message
|
6
|
+
end
|
7
|
+
|
8
|
+
def __getobj__
|
9
|
+
processed_action
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def processed_action
|
15
|
+
@processed_action ||= @action_class.new.tap do |action_object|
|
16
|
+
action_object.process @action, @message
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
module Perform
|
3
|
+
def perform!
|
4
|
+
if Caffeinate.config.enabled_drippers.nil?
|
5
|
+
Caffeinate.dripper_collection.drippers.each do |dripper|
|
6
|
+
dripper.constantize.perform!
|
7
|
+
end
|
8
|
+
else
|
9
|
+
Caffeinate.config.enabled_drippers.each do |dripper|
|
10
|
+
dripper.to_s.constantize.perform!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'caffeinate/drip_evaluator'
|
4
|
+
require 'caffeinate/schedule_evaluator'
|
5
|
+
|
6
|
+
module Caffeinate
|
7
|
+
# A PeriodicalDrip object
|
8
|
+
#
|
9
|
+
# Handles the block and provides convenience methods for the drip
|
10
|
+
class PeriodicalDrip < Drip
|
11
|
+
VALID_DRIP_OPTIONS = ALL_DRIP_OPTIONS + [:every, :until]
|
12
|
+
|
13
|
+
class << self
|
14
|
+
private def validate_drip_options(dripper, action, options)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_options(options)
|
19
|
+
options.assert_valid_keys(*VALID_DRIP_OPTIONS)
|
20
|
+
end
|
21
|
+
|
22
|
+
def normalize_options(dripper, options)
|
23
|
+
options[:mailer_class] ||= options[:mailer] || dripper.defaults[:mailer_class]
|
24
|
+
options[:using] ||= dripper.defaults[:using]
|
25
|
+
options[:step] ||= dripper.drips.size + 1
|
26
|
+
|
27
|
+
unless options.key?(:every)
|
28
|
+
raise "Periodical drips must have an `every` option."
|
29
|
+
end
|
30
|
+
|
31
|
+
options
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def every
|
36
|
+
options[:every]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
# The RSpec module contains RSpec-specific functionality for Caffeinate.
|
3
|
+
module RSpec
|
4
|
+
module Matchers
|
5
|
+
# Check if the subject subscribes to a given campaign. Only checks for presence.
|
6
|
+
#
|
7
|
+
# @param expected_campaign [Caffeinate::Campaign] The campaign to be passed as an argument to BeSubscribedTo new.
|
8
|
+
# This can be easily accessed via `UserOnboardingDripper.campaign`
|
9
|
+
# @return [BeSubscribedTo] A new BeSubscribedTo instance with the expected campaign as its argument.
|
10
|
+
def be_subscribed_to_caffeinate_campaign(expected_campaign)
|
11
|
+
BeSubscribedToCaffeinateCampaign.new(expected_campaign)
|
12
|
+
end
|
13
|
+
|
14
|
+
class BeSubscribedToCaffeinateCampaign
|
15
|
+
def initialize(expected_campaign)
|
16
|
+
@expected_campaign = expected_campaign
|
17
|
+
end
|
18
|
+
|
19
|
+
def description
|
20
|
+
"be subscribed to the \"Campaign##{@expected_campaign.slug}\" campaign"
|
21
|
+
end
|
22
|
+
|
23
|
+
def failure_message
|
24
|
+
"expected #{@hopeful_subscriber.inspect} to be subscribed to the \"Campaign##{@expected_campaign.slug}\" campaign but wasn't"
|
25
|
+
end
|
26
|
+
|
27
|
+
def with(**args)
|
28
|
+
@args = args
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def matches?(hopeful_subscriber)
|
33
|
+
@hopeful_subscriber = hopeful_subscriber
|
34
|
+
@args ||= {}
|
35
|
+
@expected_campaign.caffeinate_campaign_subscriptions.exists?(subscriber: hopeful_subscriber, **@args)
|
36
|
+
end
|
37
|
+
|
38
|
+
def failure_message_when_negated
|
39
|
+
"expected #{@hopeful_subscriber.inspect} to not be subscribed to the \"Campaign##{@expected_campaign.slug}\" campaign but was"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
# The RSpec module contains RSpec-specific functionality for Caffeinate.
|
3
|
+
module RSpec
|
4
|
+
module Matchers
|
5
|
+
# Creates an RSpec matcher for testing whether an action results in a `Caffeinate::CampaignSubscription` becoming `ended?`.
|
6
|
+
#
|
7
|
+
# @param expected_campaign [Caffeinate::Campaign] The expected campaign.
|
8
|
+
# @param subscriber [Object] The subscriber being tested.
|
9
|
+
# @param args [Hash] Additional arguments passed to the Caffeinate::CampaignSubscriber.
|
10
|
+
# @option args [Object] :user The user associated with the subscriber.
|
11
|
+
# @return [UnsubscribeFromCaffeinateCampaign] The created matcher object.
|
12
|
+
def end_caffeinate_campaign_subscription(expected_campaign, subscriber, **args)
|
13
|
+
EndCaffeinateCampaignSubscription.new(expected_campaign, subscriber, **args)
|
14
|
+
end
|
15
|
+
|
16
|
+
class EndCaffeinateCampaignSubscription
|
17
|
+
def initialize(expected_campaign, subscriber, **args)
|
18
|
+
@expected_campaign = expected_campaign
|
19
|
+
@subscriber = subscriber
|
20
|
+
@args = args
|
21
|
+
end
|
22
|
+
|
23
|
+
def description
|
24
|
+
"end the CampaignSubscription of #{who} on the \"Campaign##{@expected_campaign.slug}\" campaign"
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_message
|
28
|
+
"expected the CampaignSubscription of #{who} on the \"Campaign##{@expected_campaign.slug}\" campaign to end but didn't"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Checks whether the block results in the campaign subscription becoming ended.
|
32
|
+
#
|
33
|
+
# @param block [Block] The block of code to execute.
|
34
|
+
def matches?(block)
|
35
|
+
sub = @expected_campaign.caffeinate_campaign_subscriptions.find_by(subscriber: @subscriber, **@args)
|
36
|
+
return false unless sub && !sub.ended?
|
37
|
+
|
38
|
+
block.call
|
39
|
+
sub.reload.ended?
|
40
|
+
end
|
41
|
+
|
42
|
+
def failure_message_when_negated
|
43
|
+
"expected the CampaignSubscription of #{who} on the \"Campaign##{@expected_campaign.slug}\" campaign to not end but did"
|
44
|
+
end
|
45
|
+
|
46
|
+
def supports_block_expectations?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def who
|
53
|
+
str = "#{@subscriber.class.name}##{@subscriber.to_param}"
|
54
|
+
user = @args[:user]
|
55
|
+
if user
|
56
|
+
str << "/#{user.class.name}##{user.to_param}"
|
57
|
+
end
|
58
|
+
if @args.except(:user).any?
|
59
|
+
str << "/#{@args.except(:user).inspect}"
|
60
|
+
end
|
61
|
+
str
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
# The RSpec module contains RSpec-specific functionality for Caffeinate.
|
3
|
+
module RSpec
|
4
|
+
module Matchers
|
5
|
+
# Creates an RSpec matcher for testing whether an action results in a subscribe to a specified campaign.
|
6
|
+
#
|
7
|
+
# @param expected_campaign [Caffeinate::Campaign] The expected campaign.
|
8
|
+
# @param subscriber [Object] The subscriber being tested.
|
9
|
+
# @param args [Hash] Additional arguments passed to the Caffeinate::CampaignSubscriber.
|
10
|
+
# @option args [Object] :user The user associated with the subscriber.
|
11
|
+
# @return [SubscribeToCaffeinateCampaign] The created matcher object.
|
12
|
+
def subscribe_to_caffeinate_campaign(expected_campaign, subscriber, **args)
|
13
|
+
SubscribeToCaffeinateCampaign.new(expected_campaign, subscriber, **args)
|
14
|
+
end
|
15
|
+
|
16
|
+
class SubscribeToCaffeinateCampaign
|
17
|
+
def initialize(expected_campaign, subscriber, **args)
|
18
|
+
@expected_campaign = expected_campaign
|
19
|
+
@subscriber = subscriber
|
20
|
+
@args = args
|
21
|
+
end
|
22
|
+
|
23
|
+
def description
|
24
|
+
"subscribe #{who} to the \"Campaign##{@expected_campaign.slug}\" campaign"
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_message
|
28
|
+
"expected #{who} to subscribe to the \"Campaign##{@expected_campaign.slug}\" campaign but didn't"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Checks whether the block results in a subscription to the expected campaign.
|
32
|
+
#
|
33
|
+
# @param block [Block] The block of code to execute.
|
34
|
+
def matches?(block)
|
35
|
+
return false if @expected_campaign.caffeinate_campaign_subscriptions.active.exists?(subscriber: @subscriber, **@args)
|
36
|
+
|
37
|
+
block.call
|
38
|
+
@expected_campaign.caffeinate_campaign_subscriptions.active.exists?(subscriber: @subscriber, **@args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def failure_message_when_negated
|
42
|
+
"expected #{who} to not subscribe to the \"Campaign##{@expected_campaign.slug}\" campaign but did"
|
43
|
+
end
|
44
|
+
|
45
|
+
def supports_block_expectations?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def who
|
52
|
+
str = "#{@subscriber.class.name}##{@subscriber.to_param}"
|
53
|
+
user = @args[:user]
|
54
|
+
if user
|
55
|
+
str << "/#{user.class.name}##{user.to_param}"
|
56
|
+
end
|
57
|
+
if @args.except(:user).any?
|
58
|
+
str << "/#{@args.except(:user).inspect}"
|
59
|
+
end
|
60
|
+
str
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Caffeinate
|
2
|
+
# The RSpec module contains RSpec-specific functionality for Caffeinate.
|
3
|
+
module RSpec
|
4
|
+
module Matchers
|
5
|
+
# Creates an RSpec matcher for testing whether an action results in an unsubscribe from a specified campaign.
|
6
|
+
#
|
7
|
+
# @param expected_campaign [Caffeinate::Campaign] The expected campaign.
|
8
|
+
# @param subscriber [Object] The subscriber being tested.
|
9
|
+
# @param args [Hash] Additional arguments passed to the Caffeinate::CampaignSubscriber.
|
10
|
+
# @option args [Object] :user The user associated with the subscriber.
|
11
|
+
# @return [UnsubscribeFromCaffeinateCampaign] The created matcher object.
|
12
|
+
def unsubscribe_from_caffeinate_campaign(expected_campaign, subscriber, **args)
|
13
|
+
UnsubscribeFromCaffeinateCampaign.new(expected_campaign, subscriber, **args)
|
14
|
+
end
|
15
|
+
|
16
|
+
class UnsubscribeFromCaffeinateCampaign
|
17
|
+
def initialize(expected_campaign, subscriber, **args)
|
18
|
+
@expected_campaign = expected_campaign
|
19
|
+
@subscriber = subscriber
|
20
|
+
@args = args
|
21
|
+
end
|
22
|
+
|
23
|
+
def description
|
24
|
+
"unsubscribe #{who} from the \"Campaign##{@expected_campaign.slug}\" campaign"
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_message
|
28
|
+
"expected #{who} to unsubscribe from the \"Campaign##{@expected_campaign.slug}\" campaign but didn't"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Checks whether the block results in an unsubscribe from the expected campaign.
|
32
|
+
#
|
33
|
+
# @param block [Block] The block of code to execute.
|
34
|
+
def matches?(block)
|
35
|
+
sub = @expected_campaign.caffeinate_campaign_subscriptions.active.find_by(subscriber: @subscriber, **@args)
|
36
|
+
return false unless sub && sub.subscribed?
|
37
|
+
|
38
|
+
block.call
|
39
|
+
sub.reload.unsubscribed?
|
40
|
+
end
|
41
|
+
|
42
|
+
def failure_message_when_negated
|
43
|
+
"expected #{who} to not unsubscribe from the \"Campaign##{@expected_campaign.slug}\" campaign but did"
|
44
|
+
end
|
45
|
+
|
46
|
+
def supports_block_expectations?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def who
|
53
|
+
str = "#{@subscriber.class.name}##{@subscriber.to_param}"
|
54
|
+
user = @args[:user]
|
55
|
+
if user
|
56
|
+
str << "/#{user.class.name}##{user.to_param}"
|
57
|
+
end
|
58
|
+
if @args.except(:user).any?
|
59
|
+
str << "/#{@args.except(:user).inspect}"
|
60
|
+
end
|
61
|
+
str
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'caffeinate/rspec/matchers/be_subscribed_to_caffeinate_campaign'
|
2
|
+
require 'caffeinate/rspec/matchers/subscribe_to_caffeinate_campaign'
|
3
|
+
require 'caffeinate/rspec/matchers/unsubscribe_from_caffeinate_campaign'
|
4
|
+
require 'caffeinate/rspec/matchers/end_caffeinate_campaign_subscription'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.include Caffeinate::RSpec::Matchers
|
8
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'caffeinate/rspec/matchers'
|
@@ -30,19 +30,23 @@ module Caffeinate
|
|
30
30
|
@mailing = mailing
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
|
34
34
|
def call
|
35
35
|
if periodical?
|
36
|
-
start =
|
37
|
-
|
38
|
-
|
36
|
+
start = Caffeinate.config.now.call
|
37
|
+
if options[:start]
|
38
|
+
start = OptionEvaluator.new(options[:start], self, mailing).call
|
39
|
+
end
|
40
|
+
start += OptionEvaluator.new(options[:every], self, mailing).call if mailing.caffeinate_campaign_subscription.caffeinate_mailings.size.positive?
|
41
|
+
date = start
|
39
42
|
elsif options[:on]
|
40
43
|
date = OptionEvaluator.new(options[:on], self, mailing).call
|
41
44
|
else
|
42
45
|
date = OptionEvaluator.new(options[:delay], self, mailing).call
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
end
|
47
|
+
|
48
|
+
if date.respond_to?(:from_now)
|
49
|
+
date = date.from_now
|
46
50
|
end
|
47
51
|
|
48
52
|
if options[:at]
|
@@ -52,7 +56,7 @@ module Caffeinate
|
|
52
56
|
|
53
57
|
date
|
54
58
|
end
|
55
|
-
|
59
|
+
|
56
60
|
def respond_to_missing?(name, include_private = false)
|
57
61
|
@drip.respond_to?(name, include_private)
|
58
62
|
end
|
@@ -60,11 +64,11 @@ module Caffeinate
|
|
60
64
|
def method_missing(method, *args, &block)
|
61
65
|
@drip.send(method, *args, &block)
|
62
66
|
end
|
63
|
-
|
67
|
+
|
64
68
|
private
|
65
69
|
|
66
70
|
def periodical?
|
67
|
-
|
71
|
+
@drip.type.periodical?
|
68
72
|
end
|
69
73
|
end
|
70
74
|
end
|
data/lib/caffeinate/version.rb
CHANGED
data/lib/caffeinate.rb
CHANGED
@@ -11,9 +11,12 @@ require 'active_support'
|
|
11
11
|
require railtie
|
12
12
|
end
|
13
13
|
|
14
|
+
require 'caffeinate/perform'
|
14
15
|
require 'caffeinate/mail_ext'
|
15
16
|
require 'caffeinate/engine'
|
16
17
|
require 'caffeinate/drip'
|
18
|
+
require 'caffeinate/action_proxy'
|
19
|
+
require 'caffeinate/periodical_drip'
|
17
20
|
require 'caffeinate/url_helpers'
|
18
21
|
require 'caffeinate/configuration'
|
19
22
|
require 'caffeinate/dripper/base'
|
@@ -21,6 +24,8 @@ require 'caffeinate/deliver_async'
|
|
21
24
|
require 'caffeinate/dripper_collection'
|
22
25
|
|
23
26
|
module Caffeinate
|
27
|
+
extend Perform
|
28
|
+
|
24
29
|
def self.dripper_collection
|
25
30
|
@dripper_collection ||= DripperCollection.new
|
26
31
|
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: 2.
|
4
|
+
version: '2.4'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Brody
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -153,6 +153,7 @@ files:
|
|
153
153
|
- app/models/caffeinate/campaign.rb
|
154
154
|
- app/models/caffeinate/campaign_subscription.rb
|
155
155
|
- app/models/caffeinate/mailing.rb
|
156
|
+
- app/services/campaign_subscriptions/refuel_service.rb
|
156
157
|
- app/views/caffeinate/campaign_subscriptions/subscribe.html.erb
|
157
158
|
- app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb
|
158
159
|
- app/views/layouts/_caffeinate.html.erb
|
@@ -163,6 +164,7 @@ files:
|
|
163
164
|
- lib/caffeinate/action_mailer/extension.rb
|
164
165
|
- lib/caffeinate/action_mailer/interceptor.rb
|
165
166
|
- lib/caffeinate/action_mailer/observer.rb
|
167
|
+
- lib/caffeinate/action_proxy.rb
|
166
168
|
- lib/caffeinate/active_record/extension.rb
|
167
169
|
- lib/caffeinate/configuration.rb
|
168
170
|
- lib/caffeinate/deliver_async.rb
|
@@ -185,6 +187,15 @@ files:
|
|
185
187
|
- lib/caffeinate/engine.rb
|
186
188
|
- lib/caffeinate/helpers.rb
|
187
189
|
- lib/caffeinate/mail_ext.rb
|
190
|
+
- lib/caffeinate/message_handler.rb
|
191
|
+
- lib/caffeinate/perform.rb
|
192
|
+
- lib/caffeinate/periodical_drip.rb
|
193
|
+
- lib/caffeinate/rspec.rb
|
194
|
+
- lib/caffeinate/rspec/matchers.rb
|
195
|
+
- lib/caffeinate/rspec/matchers/be_subscribed_to_caffeinate_campaign.rb
|
196
|
+
- lib/caffeinate/rspec/matchers/end_caffeinate_campaign_subscription.rb
|
197
|
+
- lib/caffeinate/rspec/matchers/subscribe_to_caffeinate_campaign.rb
|
198
|
+
- lib/caffeinate/rspec/matchers/unsubscribe_from_caffeinate_campaign.rb
|
188
199
|
- lib/caffeinate/schedule_evaluator.rb
|
189
200
|
- lib/caffeinate/url_helpers.rb
|
190
201
|
- lib/caffeinate/version.rb
|