caffeinate 0.1.1 → 0.2.1

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -44
  3. data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +17 -2
  4. data/app/models/caffeinate/campaign.rb +40 -1
  5. data/app/models/caffeinate/campaign_subscription.rb +30 -6
  6. data/app/models/caffeinate/mailing.rb +18 -2
  7. data/app/views/caffeinate/campaign_subscriptions/subscribe.html.erb +3 -0
  8. data/app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb +3 -0
  9. data/app/views/layouts/caffeinate.html.erb +11 -0
  10. data/config/locales/en.yml +6 -0
  11. data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +1 -0
  12. data/lib/caffeinate.rb +11 -0
  13. data/lib/caffeinate/action_mailer.rb +4 -4
  14. data/lib/caffeinate/action_mailer/extension.rb +17 -1
  15. data/lib/caffeinate/action_mailer/interceptor.rb +3 -1
  16. data/lib/caffeinate/action_mailer/observer.rb +3 -3
  17. data/lib/caffeinate/active_record/extension.rb +3 -2
  18. data/lib/caffeinate/configuration.rb +3 -1
  19. data/lib/caffeinate/drip.rb +10 -35
  20. data/lib/caffeinate/drip_evaluator.rb +35 -0
  21. data/lib/caffeinate/dripper/base.rb +9 -14
  22. data/lib/caffeinate/dripper/batching.rb +20 -0
  23. data/lib/caffeinate/dripper/callbacks.rb +57 -2
  24. data/lib/caffeinate/dripper/campaign.rb +6 -8
  25. data/lib/caffeinate/dripper/defaults.rb +3 -2
  26. data/lib/caffeinate/dripper/delivery.rb +2 -2
  27. data/lib/caffeinate/dripper/drip.rb +20 -6
  28. data/lib/caffeinate/dripper/inferences.rb +29 -0
  29. data/lib/caffeinate/dripper/perform.rb +11 -4
  30. data/lib/caffeinate/dripper/subscriber.rb +2 -2
  31. data/lib/caffeinate/engine.rb +7 -1
  32. data/lib/caffeinate/helpers.rb +24 -0
  33. data/lib/caffeinate/url_helpers.rb +10 -0
  34. data/lib/caffeinate/version.rb +1 -1
  35. data/lib/generators/caffeinate/install_generator.rb +5 -1
  36. data/lib/generators/caffeinate/templates/{application_campaign.rb → application_dripper.rb} +0 -0
  37. data/lib/generators/caffeinate/templates/caffeinate.rb +11 -1
  38. metadata +12 -6
  39. data/app/views/layouts/caffeinate/application.html.erb +0 -15
  40. data/app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb +0 -1
  41. data/lib/caffeinate/action_mailer/helpers.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8395725b8aadbadd9e4531ca57c45b1f2d5cb1655568d0dc293e75fd9d96d027
4
- data.tar.gz: c630d7fa84abfd1ad3d402a7e299696ad9ebe783d8e9a975f35680026a7ca3f3
3
+ metadata.gz: 3241ca7ca0c6e31220511ad04172105d97058fa84f1de1e6ac442052823a91e0
4
+ data.tar.gz: 5b0bda6f08ae89f79cbd64df4c75f48fcb0dacf7471155aae779290b6a1b2e41
5
5
  SHA512:
6
- metadata.gz: 9b507434acc98924a29e541de7d0cd239ae72e6462b620f76c829b640644a0bf6bf8ccff46a0fbecfe788e934db11d313384169d77576ef1fd32ddbc7082c8cd
7
- data.tar.gz: 6ec96833ba77d9ff66c0114d6911f8d5e17eed36c0c596541fdc3055b5c211f95f7983c3a56a183fc5090baefe4f2c5a31f5b87926a7b0cc8989d007f4df6e79
6
+ metadata.gz: d43a0030851c7fa107fed83fcc05c9cfa8596be068a8327d01f597a0a45fc84aa403d1a95990e8a4c45013fb44bc899215778d36ce8a8f5cc770db50b33cd0df
7
+ data.tar.gz: 6f9e399cc7e0a00bbb544b668d80e0438f4482013f3163a819b9f2ab8ea502f3c8535aacb91d57eb80efc5f12c22bd03a2bdfd36c2216544bb9aa1fd6928e797
data/README.md CHANGED
@@ -1,81 +1,89 @@
1
1
  # Caffeinate
2
2
 
3
- Ruby on Rails drip campaign engine.
3
+ Caffeinate is a drip campaign engine for Ruby on Rails applications.
4
4
 
5
- ## Are there docs?
6
-
7
- [Since you asked](https://rubydoc.info/github/joshmn/caffeinate).
5
+ Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
6
+ and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
7
+ already familiar with: Ruby on Rails.
8
8
 
9
9
  ## Usage
10
10
 
11
- Given a mailer like this:
11
+ You can probably imagine seeing a Mailer like this:
12
12
 
13
13
  ```ruby
14
- class AbandonedCartMailer < ActionMailer::Base
15
- def you_forgot_something(cart)
16
- mail(to: cart.user.email, subject: "You forgot something!")
14
+ class OnboardingMailer < ActionMailer::Base
15
+ # Send on account creation
16
+ def welcome_to_my_cool_app(user)
17
+ mail(to: user.email, subject: "You forgot something!")
17
18
  end
18
19
 
19
- def selling_out_soon(cart)
20
- mail(to: cart.user.email, subject: "Selling out soon!")
20
+ # Send 2 days after the user signs up
21
+ def some_cool_tips(user)
22
+ mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
23
+ end
24
+
25
+ # Sends 3 days after the user signs up and hasn't added a company profile yet
26
+ def help_getting_started(user)
27
+ return if user.company.present?
28
+
29
+ mail(to: user.email, subject: "Did you know...")
21
30
  end
22
31
  end
23
32
  ```
24
33
 
34
+ With background jobs running, checking, and everything else. That's messy. Why are we checking state in the Mailer? Ugh.
35
+
36
+ We can clean this up with Caffeinate. Here's how we'd do it.
37
+
25
38
  ### Create a Campaign
26
39
 
27
40
  ```ruby
28
- Caffeinate::Campaign.create!(name: "Abandoned Cart", slug: "abandoned_cart")
41
+ Caffeinate::Campaign.create!(name: "Onboarding Campaign", slug: "onboarding")
29
42
  ```
30
43
 
31
44
  ### Create a Caffeinate::Dripper
32
45
 
46
+ Place the contents below in `app/drippers/onboarding_dripper.rb`:
47
+
33
48
  ```ruby
34
- class AbandonedCartDripper < Caffeinate::Dripper::Base
35
- # This should match a Caffeinate::Campaign#slug
36
- campaign :abandoned_cart
37
-
38
- # A block to subscribe your users automatically
39
- # You can invoke this by calling `AbandonedCartDripper.subscribe!`,
40
- # probably in a background process, run at a given interval
41
- subscribes do
42
- Cart.left_joins(:cart_items)
43
- .includes(:user)
44
- .where(completed_at: nil)
45
- .where(updated_at: 1.day.ago..2.days.ago)
46
- .having('count(cart_items.id) = 0').each do |cart|
47
- subscribe(cart, user: cart.user)
48
- end
49
- end
50
-
51
- # Register your drips! Syntax is
52
- # drip <mailer_action_name>, mailer: <MailerClass>, delay: <ActiveSupport::Interval>
53
- drip :you_forgot_something, mailer: "AbandonedCartMailer", delay: 1.hour
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
49
+ class OnboardingDripper < ApplicationDripper
50
+ drip :welcome_to_my_cool_app, delay: 0.hours
51
+ drip :some_cool_tips, delay: 2.days
52
+ drip :help_getting_started, delay: 3.days do
53
+ if mailing.user.company.present?
54
+ mailing.unsubscribe!
58
55
  return false
59
- end
60
- end
56
+ end
57
+ end
61
58
  end
62
59
  ```
63
60
 
64
- Automatically subscribe eligible carts to it by running:
61
+ ### Add a subscriber to the Campaign
65
62
 
66
63
  ```ruby
67
- AbandonedCartDripper.subscribe!
64
+ class User < ApplicationRecord
65
+ after_create_commit do
66
+ Caffeinate::Campaign.find_by(slug: "onboarding").subscribe(self)
67
+ end
68
+ end
68
69
  ```
69
70
 
70
- This would typically run in a background job, queued up at a given interval.
71
+ ### Run the Dripper
71
72
 
72
- And then, once it's done, start your engines!
73
+ You'd normally want to do this in a cron/whenever/scheduled Sidekiq/etc job.
73
74
 
74
- ```ruby
75
- AbandonedCartDripper.perform!
75
+ ```ruby
76
+ OnboardingDripper.perform!
76
77
  ```
77
78
 
78
- This, too, would typically run in a background job, queued up at a given interval.
79
+ ### Spend more time building
80
+
81
+ Now you can spend more time building your app and less time managing your marketing campaigns.
82
+ * Centralized logic makes it easy to understand the flow
83
+ * Subscription management, timings, send history all built-in
84
+ * Built on the stack you're already familiar with
85
+
86
+ There's a lot more than what you just saw, too! Caffeinate almost makes managing timed email sequences fun.
79
87
 
80
88
  ## Installation
81
89
 
@@ -103,6 +111,11 @@ Followed by a migrate:
103
111
  $ rails db:migrate
104
112
  ```
105
113
 
114
+ ## Documentation
115
+
116
+ * [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
117
+ * [Better-than-average code documentation](https://rubydoc.info/github/joshmn/caffeinate)
118
+
106
119
  ## Upcoming features/todo
107
120
 
108
121
  * Ability to optionally use relative start time when creating a step
@@ -2,18 +2,33 @@
2
2
 
3
3
  module Caffeinate
4
4
  class CampaignSubscriptionsController < ApplicationController
5
+ layout 'caffeinate'
6
+
7
+ helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
8
+
5
9
  before_action :find_campaign_subscription!
6
10
 
7
11
  def unsubscribe
8
12
  @campaign_subscription.unsubscribe!
9
- render plain: 'You have been unsubscribed.'
13
+ end
14
+
15
+ def subscribe
16
+ @campaign_subscription.subscribe!
10
17
  end
11
18
 
12
19
  private
13
20
 
21
+ def caffeinate_subscribe_url(**options)
22
+ Caffeinate::UrlHelpers.caffeinate_subscribe_url(@campaign_subscription, options)
23
+ end
24
+
25
+ def caffeinate_unsubscribe_url
26
+ Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(@campaign_subscription, options)
27
+ end
28
+
14
29
  def find_campaign_subscription!
15
30
  @campaign_subscription = ::Caffeinate::CampaignSubscription.find_by(token: params[:token])
16
- return render plain: '404' if @campaign_subscription.nil?
31
+ raise ::ActiveRecord::RecordNotFound if @campaign_subscription.nil?
17
32
  end
18
33
  end
19
34
  end
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # == Schema Information
4
+ #
5
+ # Table name: caffeinate_campaigns
6
+ #
7
+ # id :integer not null, primary key
8
+ # name :string not null
9
+ # slug :string not null
10
+ # created_at :datetime not null
11
+ # updated_at :datetime not null
12
+ #
3
13
  module Caffeinate
4
- # Campaign.
14
+ # Campaign ties together subscribers and mailings, and provides one core model for handling your Drippers.
5
15
  class Campaign < ApplicationRecord
6
16
  self.table_name = 'caffeinate_campaigns'
7
17
  has_many :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
@@ -12,9 +22,38 @@ module Caffeinate
12
22
  Caffeinate.dripper_to_campaign_class[slug.to_sym].constantize
13
23
  end
14
24
 
25
+ # Convenience method for find_by!(slug: value)
26
+ #
27
+ # ::Caffeinate::Campaign[:onboarding]
28
+ # # is the same as
29
+ # ::Caffeinate::Campaign.find_by(slug: :onboarding)
30
+ def self.[](val)
31
+ find_by!(slug: val)
32
+ end
33
+
34
+ # Checks to see if the subscriber exists.
35
+ #
36
+ # Use `find_by` so that we don't have to load the record twice. Often used with `subscribes?`
37
+ def subscriber(record, **args)
38
+ @subscriber ||= caffeinate_campaign_subscriptions.find_by(subscriber: record, **args)
39
+ end
40
+
41
+ # Check if the subscriber exists
42
+ def subscribes?(record, **args)
43
+ subscriber(record, **args).present?
44
+ end
45
+
15
46
  # Subscribes an object to a campaign.
16
47
  def subscribe(subscriber, **args)
17
48
  caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
18
49
  end
50
+
51
+ # Subscribes an object to a campaign.
52
+ def subscribe!(subscriber, **args)
53
+ subscription = subscribe(subscriber, **args)
54
+ return subscription if subscribe.persisted?
55
+
56
+ raise ActiveRecord::RecordInvalid, subscription
57
+ end
19
58
  end
20
59
  end
@@ -1,12 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # == Schema Information
4
+ #
5
+ # Table name: caffeinate_campaign_subscriptions
6
+ #
7
+ # id :integer not null, primary key
8
+ # caffeinate_campaign_id :integer not null
9
+ # subscriber_type :string not null
10
+ # subscriber_id :string not null
11
+ # user_type :string
12
+ # user_id :string
13
+ # token :string not null
14
+ # ended_at :datetime
15
+ # unsubscribed_at :datetime
16
+ # created_at :datetime not null
17
+ # updated_at :datetime not null
18
+ #
3
19
  module Caffeinate
4
- # CampaignSubscription associates an object and its optional user to a Campaign.
20
+ # CampaignSubscription associates an object and its optional user to a Campaign
21
+ # and its relevant Mailings.
5
22
  class CampaignSubscription < ApplicationRecord
6
23
  self.table_name = 'caffeinate_campaign_subscriptions'
7
24
 
8
- has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
9
- has_one :next_caffeinate_mailing, -> { upcoming.unsent.limit(1).first }, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
25
+ has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
26
+ has_one :next_caffeinate_mailing, -> { upcoming.unsent }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
10
27
  belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
11
28
  belongs_to :subscriber, polymorphic: true
12
29
  belongs_to :user, polymorphic: true, optional: true
@@ -37,12 +54,12 @@ module Caffeinate
37
54
  !ended? && !unsubscribed?
38
55
  end
39
56
 
40
- # Checks if the CampaignSubscription is not subscribed
57
+ # Checks if the CampaignSubscription is not subscribed by checking the presence of `unsubscribed_at`
41
58
  def unsubscribed?
42
- !subscribed?
59
+ unsubscribed_at.present?
43
60
  end
44
61
 
45
- # Checks if the CampaignSubscription is ended
62
+ # Checks if the CampaignSubscription is ended by checking the presence of `ended_at`
46
63
  def ended?
47
64
  ended_at.present?
48
65
  end
@@ -61,6 +78,13 @@ module Caffeinate
61
78
  caffeinate_campaign.to_dripper.run_callbacks(:on_unsubscribe, self)
62
79
  end
63
80
 
81
+ # Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks
82
+ def resubscribe!
83
+ update!(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
84
+
85
+ caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
86
+ end
87
+
64
88
  private
65
89
 
66
90
  # Create mailings according to the drips registered in the Campaign
@@ -1,8 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # == Schema Information
4
+ #
5
+ # Table name: caffeinate_mailings
6
+ #
7
+ # id :integer not null, primary key
8
+ # caffeinate_campaign_subscription_id :integer not null
9
+ # send_at :datetime
10
+ # sent_at :datetime
11
+ # skipped_at :datetime
12
+ # mailer_class :string not null
13
+ # mailer_action :string not null
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ #
3
17
  module Caffeinate
4
18
  # Records of the mails sent and to be sent for a given `::Caffeinate::CampaignSubscriber`
5
19
  class Mailing < ApplicationRecord
20
+ CURRENT_THREAD_KEY = :current_caffeinate_mailing
21
+
6
22
  self.table_name = 'caffeinate_mailings'
7
23
 
8
24
  belongs_to :caffeinate_campaign_subscription, class_name: 'Caffeinate::CampaignSubscription'
@@ -49,7 +65,7 @@ module Caffeinate
49
65
  # The associated drip
50
66
  # @todo This can be optimized with a better cache
51
67
  def drip
52
- @drip ||= caffeinate_campaign.to_dripper.drips.find { |drip| drip.action.to_s == mailer_action }
68
+ @drip ||= caffeinate_campaign.to_dripper.drip_collection[mailer_action]
53
69
  end
54
70
 
55
71
  # The associated Subscriber from `::Caffeinate::CampaignSubscription`
@@ -64,7 +80,7 @@ module Caffeinate
64
80
 
65
81
  # Assigns attributes to the Mailing from the Drip
66
82
  def from_drip(drip)
67
- self.send_at = drip.options[:delay].from_now
83
+ self.send_at = drip.send_at
68
84
  self.mailer_class = drip.options[:mailer_class]
69
85
  self.mailer_action = drip.action
70
86
  self
@@ -0,0 +1,3 @@
1
+ <%= t("caffeinate.campaign_subscriptions.subscribe") %>
2
+ <p><%= t("caffeinate.campaign_subscriptions.changed_your_mind")%></p>
3
+ <p><%= link_to "Unsubscribe", caffeinate_subscribe_url %></p>
@@ -0,0 +1,3 @@
1
+ <h5><%= t("caffeinate.campaign_subscriptions.unsubscribe")%></h5>
2
+ <p><%= t("caffeinate.campaign_subscriptions.changed_your_mind")%></p>
3
+ <p><%= link_to "Resubscribe", caffeinate_subscribe_url %></p>
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= Rails.application.class.module_parent_name %></title>
5
+ </head>
6
+ <body>
7
+
8
+ <%= yield %>
9
+
10
+ </body>
11
+ </html>
@@ -0,0 +1,6 @@
1
+ en:
2
+ caffeinate:
3
+ campaign_subscriptions:
4
+ subscribe: "You have been subscribed."
5
+ unsubscribe: "You have been unsubscribed."
6
+ changed_your_mind: "Change your mind?"
@@ -12,6 +12,7 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
12
12
  t.string :user_id
13
13
  t.string :token, null: false
14
14
  t.datetime :ended_at
15
+ t.datetime :resubscribed_at
15
16
  t.datetime :unsubscribed_at
16
17
 
17
18
  t.timestamps
@@ -3,6 +3,7 @@
3
3
  require 'rails/all'
4
4
  require 'caffeinate/engine'
5
5
  require 'caffeinate/drip'
6
+ require 'caffeinate/url_helpers'
6
7
  require 'caffeinate/configuration'
7
8
  require 'caffeinate/dripper/base'
8
9
  require 'caffeinate/deliver_async'
@@ -27,4 +28,14 @@ module Caffeinate
27
28
  def self.setup
28
29
  yield config
29
30
  end
31
+
32
+ # The current mailing
33
+ def self.current_mailing=(val)
34
+ Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY] = val
35
+ end
36
+
37
+ # The current mailing
38
+ def self.current_mailing
39
+ Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
40
+ end
30
41
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/action_mailer/extension'
4
- require 'caffeinate/action_mailer/helpers'
5
- require 'caffeinate/action_mailer/interceptor'
6
- require 'caffeinate/action_mailer/observer'
3
+ require 'mail'
4
+
5
+ # Includes all files in `caffeinate/action_mailer`
6
+ Dir["#{__dir__}/action_mailer/*"].each { |path| require "caffeinate/action_mailer/#{File.basename(path)}" }
@@ -2,11 +2,27 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
+ # Convenience for setting `@mailing`, and convenience methods for inferred `caffeinate_unsubscribe_url` and
6
+ # `caffeinate_subscribe_url`.
5
7
  module Extension
6
8
  def self.included(klass)
7
9
  klass.before_action do
8
- @mailing = Thread.current[:current_caffeinate_mailing] if Thread.current[:current_caffeinate_mailing]
10
+ @mailing = Caffeinate.current_mailing if Caffeinate.current_mailing
9
11
  end
12
+
13
+ klass.helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
14
+ end
15
+
16
+ # Assumes `@mailing` is set
17
+ def caffeinate_unsubscribe_url(mailing: nil, **options)
18
+ mailing ||= @mailing
19
+ Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(mailing.caffeinate_campaign_subscription, **options)
20
+ end
21
+
22
+ # Assumes `@mailing` is set
23
+ def caffeinate_subscribe_url(mailing: nil, **options)
24
+ mailing ||= @mailing
25
+ Caffeinate::UrlHelpers.caffeinate_subscribe_url(mailing.caffeinate_campaign_subscription, **options)
10
26
  end
11
27
  end
12
28
  end
@@ -2,10 +2,12 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
+ # Handles the evaluation of a drip against a mailing to determine if it ultimately gets delivered.
6
+ # Also invokes the `before_send` callbacks.
5
7
  class Interceptor
6
8
  # Handles `before_send` callbacks for a `Caffeinate::Dripper`
7
9
  def self.delivering_email(message)
8
- mailing = Thread.current[:current_caffeinate_mailing]
10
+ mailing = Caffeinate.current_mailing
9
11
  return unless mailing
10
12
 
11
13
  mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing.caffeinate_campaign_subscription, mailing, message)
@@ -2,14 +2,14 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
- # Handles updating the Caffeinate::Message if it's available in Thread.current[:current_caffeinate_mailing]
5
+ # Handles updating the Caffeinate::Message if it's available in Caffeinate.current_mailing
6
6
  # and runs any associated callbacks
7
7
  class Observer
8
8
  def self.delivered_email(message)
9
- mailing = Thread.current[:current_caffeinate_mailing]
9
+ mailing = Caffeinate.current_mailing
10
10
  return unless mailing
11
11
 
12
- mailing.update!(sent_at: Caffeinate.config.time_now) if message.perform_deliveries
12
+ mailing.update!(sent_at: Caffeinate.config.time_now, skipped_at: nil) if message.perform_deliveries
13
13
  mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing.caffeinate_campaign_subscription, mailing, message)
14
14
  end
15
15
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActiveRecord
5
+ # Includes the ActiveRecord association and relevant scopes for an ActiveRecord-backed model
5
6
  module Extension
6
7
  # Adds the associations for a subscriber
7
8
  def caffeinate_subscriber
8
- has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription'
9
+ has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription', dependent: :destroy
9
10
  has_many :caffeinate_campaigns, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Campaign'
10
11
  has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Mailing'
11
12
 
12
- scope :not_subscribed_to, lambda { |list|
13
+ scope :not_subscribed_to_campaign, lambda { |list|
13
14
  subscribed = ::Caffeinate::CampaignSubscription.select(:subscriber_id).joins(:caffeinate_campaign).where(caffeinate_campaigns: { slug: list }, subscriber_type: name)
14
15
  where.not(id: subscribed)
15
16
  }
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
+ # Global configuration
4
5
  class Configuration
5
- attr_accessor :now, :async_delivery, :mailing_job
6
+ attr_accessor :now, :async_delivery, :mailing_job, :batch_size
6
7
 
7
8
  def initialize
8
9
  @now = -> { Time.current }
9
10
  @async_delivery = false
10
11
  @mailing_job = nil
12
+ @batch_size = 1_000
11
13
  end
12
14
 
13
15
  def now=(val)
@@ -1,40 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'caffeinate/drip_evaluator'
3
4
  module Caffeinate
4
5
  # A Drip object
6
+ #
7
+ # Handles the block and provides convenience methods for the drip
5
8
  class Drip
6
- # Handles the block and provides convenience methods for the drip
7
- class Evaluator
8
- attr_reader :mailing
9
- def initialize(mailing)
10
- @mailing = mailing
11
- end
12
-
13
- def call(&block)
14
- return true unless block
15
-
16
- instance_eval(&block)
17
- end
18
-
19
- # Ends the CampaignSubscription
20
- def end!
21
- mailing.caffeinate_campaign_subscription.end!
22
- false
23
- end
24
-
25
- # Unsubscribes the CampaignSubscription
26
- def unsubscribe!
27
- mailing.caffeinate_campaign_subscription.unsubscribe!
28
- false
29
- end
30
-
31
- # Skips the mailing
32
- def skip!
33
- mailing.skip!
34
- false
35
- end
36
- end
37
-
38
9
  attr_reader :dripper, :action, :options, :block
39
10
  def initialize(dripper, action, options, &block)
40
11
  @dripper = dripper
@@ -43,14 +14,18 @@ module Caffeinate
43
14
  @block = block
44
15
  end
45
16
 
46
- # If the associated ActionMailer uses `ActionMailer::Parameterized` initialization
17
+ # If the associated ActionMailer uses `ActionMailer::Parameterized` initialization instead of argument-based initialization
47
18
  def parameterized?
48
19
  options[:using] == :parameterized
49
20
  end
50
21
 
51
- # If the drip is enabled.
22
+ def send_at
23
+ options[:delay].from_now
24
+ end
25
+
26
+ # Checks if the drip is enabled
52
27
  def enabled?(mailing)
53
- Evaluator.new(mailing).call(&@block)
28
+ DripEvaluator.new(mailing).call(&@block)
54
29
  end
55
30
  end
56
31
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ # Handles evaluating the `drip` block and provides convenience methods for handling the mailing or its campaign.
5
+ class DripEvaluator
6
+ attr_reader :mailing
7
+ def initialize(mailing)
8
+ @mailing = mailing
9
+ end
10
+
11
+ def call(&block)
12
+ return true unless block
13
+
14
+ instance_eval(&block)
15
+ end
16
+
17
+ # Ends the CampaignSubscription
18
+ def end!
19
+ mailing.caffeinate_campaign_subscription.end!
20
+ false
21
+ end
22
+
23
+ # Unsubscribes the CampaignSubscription
24
+ def unsubscribe!
25
+ mailing.caffeinate_campaign_subscription.unsubscribe!
26
+ false
27
+ end
28
+
29
+ # Skips the mailing
30
+ def skip!
31
+ mailing.skip!
32
+ false
33
+ end
34
+ end
35
+ end
@@ -1,33 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/dripper/drip'
3
+ require 'caffeinate/dripper/batching'
4
4
  require 'caffeinate/dripper/callbacks'
5
- require 'caffeinate/dripper/defaults'
6
- require 'caffeinate/dripper/subscriber'
7
5
  require 'caffeinate/dripper/campaign'
8
- require 'caffeinate/dripper/perform'
6
+ require 'caffeinate/dripper/defaults'
9
7
  require 'caffeinate/dripper/delivery'
8
+ require 'caffeinate/dripper/drip'
9
+ require 'caffeinate/dripper/inferences'
10
+ require 'caffeinate/dripper/perform'
11
+ require 'caffeinate/dripper/subscriber'
10
12
 
11
13
  module Caffeinate
12
14
  module Dripper
15
+ # Base class
13
16
  class Base
17
+ include Batching
14
18
  include Callbacks
15
19
  include Campaign
16
20
  include Defaults
17
21
  include Delivery
18
22
  include Drip
23
+ include Inferences
19
24
  include Perform
20
25
  include Subscriber
21
-
22
- # The inferred mailer class
23
- def self.inferred_mailer_class
24
- klass_name = "#{name.delete_suffix('Dripper')}Mailer"
25
- klass = klass_name.safe_constantize
26
- return nil unless klass
27
- return klass_name if klass < ::ActionMailer::Base
28
-
29
- nil
30
- end
31
26
  end
32
27
  end
33
28
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ # Includes batch support for setting the batch size for Perform
5
+ module Batching
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def batch_size(num)
12
+ @_batch_size = num
13
+ end
14
+
15
+ def _batch_size
16
+ @_batch_size || ::Caffeinate.config.batch_size
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,12 +2,17 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Callbacks for a Dripper.
5
6
  module Callbacks
6
7
  # :nodoc:
7
8
  def self.included(klass)
8
9
  klass.extend ClassMethods
9
10
  end
10
11
 
12
+ def run_callbacks(name, *args)
13
+ self.class.run_callbacks(name, *args)
14
+ end
15
+
11
16
  module ClassMethods
12
17
  # :nodoc:
13
18
  def run_callbacks(name, *args)
@@ -33,6 +38,56 @@ module Caffeinate
33
38
  @on_subscribe_blocks ||= []
34
39
  end
35
40
 
41
+ # Callback before the mailings get processed.
42
+ #
43
+ # before_process do |dripper|
44
+ # Slack.notify(:caffeinate, "Dripper is getting ready for mailing! #{dripper.caffeinate_campaign.name}!")
45
+ # end
46
+ #
47
+ # @yield Caffeinate::Dripper
48
+ def before_process(&block)
49
+ before_process_blocks << block
50
+ end
51
+
52
+ # :nodoc:
53
+ def before_process_blocks
54
+ @before_process_blocks ||= []
55
+ end
56
+
57
+ # Callback before the mailings get processed in a batch.
58
+ #
59
+ # after_process do |dripper, mailings|
60
+ # Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
61
+ # end
62
+ #
63
+ # @yield Caffeinate::Dripper
64
+ # @yield Caffeinate::Mailing [Array]
65
+ def on_process(&block)
66
+ on_process_blocks << block
67
+ end
68
+
69
+ # :nodoc:
70
+ def on_process_blocks
71
+ @on_process_blocks ||= []
72
+ end
73
+
74
+ # Callback after the all the mailings have been sent.
75
+ #
76
+ # after_process do |dripper|
77
+ # Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
78
+ # end
79
+ #
80
+ # @yield Caffeinate::Dripper
81
+ # @yield Caffeinate::Mailing [Array]
82
+ def after_process(&block)
83
+ after_process_blocks << block
84
+ end
85
+
86
+ # :nodoc:
87
+ def after_process_blocks
88
+ @after_process_blocks ||= []
89
+ end
90
+
36
91
  # Callback before a Drip has called the mailer.
37
92
  #
38
93
  # before_drip do |campaign_subscription, mailing, drip|
@@ -125,8 +180,8 @@ module Caffeinate
125
180
  # Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
126
181
  # end
127
182
  #
128
- # @yield Caffeinate::CampaignSubscription
129
- # @yield Caffeinate::Mailing
183
+ # @yield `Caffeinate::CampaignSubscription`
184
+ # @yield `Caffeinate::Mailing`
130
185
  def on_skip(&block)
131
186
  on_skip_blocks << block
132
187
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Campaign methods for `Caffeinate::Dripper`.
5
6
  module Campaign
6
7
  # :nodoc:
7
8
  def self.included(klass)
@@ -10,7 +11,7 @@ module Caffeinate
10
11
 
11
12
  # The campaign for this Dripper
12
13
  #
13
- # @return Caffeinate::Campaign
14
+ # @return `Caffeinate::Campaign`
14
15
  def campaign
15
16
  self.class.caffeinate_campaign
16
17
  end
@@ -18,7 +19,7 @@ module Caffeinate
18
19
  module ClassMethods
19
20
  # Sets the campaign on the Dripper and resets any existing `@caffeinate_campaign`
20
21
  #
21
- # class OrdersDripper
22
+ # class OrdersDripper < ApplicationDripper
22
23
  # campaign :order_drip
23
24
  # end
24
25
  #
@@ -33,19 +34,16 @@ module Caffeinate
33
34
  Caffeinate.register_dripper(@_campaign_slug, name)
34
35
  end
35
36
 
36
- # Returns the `Caffeinate::Campaign` object for the Campaign
37
+ # Returns the `Caffeinate::Campaign` object for the Dripper
37
38
  def caffeinate_campaign
38
39
  return @caffeinate_campaign if @caffeinate_campaign.present?
39
40
 
40
- @caffeinate_campaign = ::Caffeinate::Campaign.find_by(slug: campaign_slug)
41
- return @caffeinate_campaign if @caffeinate_campaign
42
-
43
- raise(::ActiveRecord::RecordNotFound, "Unable to find ::Caffeinate::Campaign with slug #{campaign_slug}.")
41
+ @caffeinate_campaign = ::Caffeinate::Campaign[campaign_slug]
44
42
  end
45
43
 
46
44
  # The defined slug or the inferred slug
47
45
  def campaign_slug
48
- @_campaign_slug || name.delete_suffix('Campaign')
46
+ @_campaign_slug || inferred_campaign_slug
49
47
  end
50
48
  end
51
49
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Handles the default DSL for a `Caffeinate::Dripper`.
5
6
  module Defaults
6
7
  # :nodoc:
7
8
  def self.included(klass)
@@ -11,7 +12,7 @@ module Caffeinate
11
12
  module ClassMethods
12
13
  # The defaults set in the Campaign
13
14
  def defaults
14
- @defaults ||= { mailer_class: inferred_mailer_class }
15
+ @defaults ||= { mailer_class: inferred_mailer_class, batch_size: ::Caffeinate.config.batch_size }
15
16
  end
16
17
 
17
18
  # The default options for the Campaign
@@ -23,7 +24,7 @@ module Caffeinate
23
24
  # @param [Hash] options The options to set defaults with
24
25
  # @option options [String] :mailer_class The mailer class
25
26
  def default(options = {})
26
- options.assert_valid_keys(:mailer_class, :mailer)
27
+ options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
27
28
  @defaults = options
28
29
  end
29
30
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # Handles delivery of a Caffeinate::Mailer for a Caffeinate::Dripper
5
+ # Handles delivery of a `Caffeinate::Mailer` for a `Caffeinate::Dripper`.
6
6
  module Delivery
7
7
  # :nodoc:
8
8
  def self.included(klass)
@@ -14,7 +14,7 @@ module Caffeinate
14
14
  #
15
15
  # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
16
  def deliver!(mailing)
17
- Thread.current[:current_caffeinate_mailing] = mailing
17
+ Caffeinate.current_mailing = mailing
18
18
 
19
19
  if mailing.drip.parameterized?
20
20
  mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action).deliver
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # The Drip DSL for registering a drip
5
+ # The Drip DSL for registering a drip.
6
6
  module Drip
7
7
  # A collection of Drip objects for a `Caffeinate::Dripper`
8
8
  class DripCollection
@@ -10,21 +10,29 @@ module Caffeinate
10
10
 
11
11
  def initialize(dripper)
12
12
  @dripper = dripper
13
- @drips = []
13
+ @drips = {}
14
14
  end
15
15
 
16
16
  # Register the drip
17
17
  def register(action, options, &block)
18
- @drips << ::Caffeinate::Drip.new(@dripper, action, options, &block)
18
+ @drips[action.to_s] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
19
19
  end
20
20
 
21
21
  def each(&block)
22
- @drips.each { |drip| block.call(drip) }
22
+ @drips.each { |action_name, drip| block.call(action_name, drip) }
23
+ end
24
+
25
+ def values
26
+ @drips.values
23
27
  end
24
28
 
25
29
  def size
26
30
  @drips.size
27
31
  end
32
+
33
+ def [](val)
34
+ @drips[val]
35
+ end
28
36
  end
29
37
 
30
38
  # :nodoc:
@@ -33,9 +41,14 @@ module Caffeinate
33
41
  end
34
42
 
35
43
  module ClassMethods
44
+ # A collection of Drip objects associated with a given `Caffeinate::Dripper`
45
+ def drip_collection
46
+ @drip_collection ||= DripCollection.new(self)
47
+ end
48
+
36
49
  # A collection of Drip objects associated with a given `Caffeinate::Dripper`
37
50
  def drips
38
- @drips ||= DripCollection.new(self)
51
+ drip_collection.values
39
52
  end
40
53
 
41
54
  # Register a drip on the Dripper
@@ -50,6 +63,7 @@ module Caffeinate
50
63
  def drip(action_name, options = {}, &block)
51
64
  options.assert_valid_keys(:mailer_class, :step, :delay, :using, :mailer)
52
65
  options[:mailer_class] ||= options[:mailer] || defaults[:mailer_class]
66
+ options[:using] ||= defaults[:using]
53
67
  options[:step] ||= drips.size + 1
54
68
 
55
69
  if options[:mailer_class].nil?
@@ -57,7 +71,7 @@ module Caffeinate
57
71
  end
58
72
  raise ArgumentError, "You must define :delay in the options for :#{action_name}" if options[:delay].nil?
59
73
 
60
- drips.register(action_name, options, &block)
74
+ drip_collection.register(action_name, options, &block)
61
75
  end
62
76
  end
63
77
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Includes the inferred methods based on a Dripper name.
6
+ module Inferences
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # The inferred mailer class
13
+ def inferred_mailer_class
14
+ klass_name = "#{name.delete_suffix('Dripper')}Mailer"
15
+ klass = klass_name.safe_constantize
16
+ return nil unless klass
17
+ return klass_name if klass < ::ActionMailer::Base
18
+
19
+ nil
20
+ end
21
+
22
+ # The inferred mailer class
23
+ def inferred_campaign_slug
24
+ name.delete_suffix('Dripper').to_s.underscore
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`
5
+ # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`.
6
6
  module Perform
7
7
  # :nodoc:
8
8
  def self.included(klass)
@@ -11,14 +11,21 @@ module Caffeinate
11
11
 
12
12
  # Delivers the next_caffeinate_mailer for the campaign's subscribers.
13
13
  #
14
+ # Handles with batches based on batch_size.
15
+ #
14
16
  # OrderDripper.new.perform!
15
17
  #
16
18
  # @return nil
17
19
  def perform!
18
- campaign.caffeinate_campaign_subscriptions.joins(:next_caffeinate_mailing).includes(:next_caffeinate_mailing).each do |subscriber|
19
- subscriber.next_caffeinate_mailing.process!
20
+ run_callbacks(:before_process, self)
21
+ campaign.caffeinate_campaign_subscriptions.active.in_batches(of: self.class._batch_size).each do |batch|
22
+ run_callbacks(:on_process, self, batch)
23
+ batch.each do |subscriber|
24
+ subscriber.next_caffeinate_mailing&.process!
25
+ end
20
26
  end
21
- true
27
+ run_callbacks(:after_process, self)
28
+ nil
22
29
  end
23
30
 
24
31
  module ClassMethods
@@ -30,8 +30,8 @@ module Caffeinate
30
30
  # @option [ActiveRecord::Base] :user The associated user (optional)
31
31
  #
32
32
  # @return [Caffeinate::CampaignSubscriber] the created CampaignSubscriber
33
- def subscribe(subscriber, user:)
34
- caffeinate_campaign.subscribe(subscriber, user: user)
33
+ def subscribe(subscriber, **args)
34
+ caffeinate_campaign.subscribe(subscriber, **args)
35
35
  end
36
36
 
37
37
  # :nodoc:
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'caffeinate/helpers'
3
4
  require 'caffeinate/action_mailer'
4
5
  require 'caffeinate/active_record/extension'
5
6
 
6
7
  module Caffeinate
7
- # :nodoc:
8
+ # Adds Caffeinate to Rails
8
9
  class Engine < ::Rails::Engine
9
10
  isolate_namespace Caffeinate
11
+ config.eager_load_namespaces << Caffeinate
10
12
 
11
13
  ActiveSupport.on_load(:action_mailer) do
12
14
  include ::Caffeinate::ActionMailer::Extension
@@ -17,5 +19,9 @@ module Caffeinate
17
19
  ActiveSupport.on_load(:active_record) do
18
20
  extend ::Caffeinate::ActiveRecord::Extension
19
21
  end
22
+
23
+ ActiveSupport.on_load(:action_view) do
24
+ ApplicationHelper.include ::Caffeinate::Helpers
25
+ end
20
26
  end
21
27
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ # URL helpers for accessing the mounted Caffeinate instance.
5
+ module Helpers
6
+ def caffeinate_unsubscribe_url(subscription, **options)
7
+ opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
8
+ Caffeinate::Engine.routes.url_helpers.unsubscribe_campaign_subscription_url(token: subscription.token, **opts)
9
+ end
10
+
11
+ def caffeinate_subscribe_url(subscription, **options)
12
+ opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
13
+ Caffeinate::Engine.routes.url_helpers.subscribe_campaign_subscription_url(token: subscription.token, **opts)
14
+ end
15
+
16
+ def caffeinate_unsubscribe_path(subscription, **options)
17
+ Caffeinate::Engine.routes.url_helpers.unsubscribe_campaign_subscription_path(token: subscription.token, **options)
18
+ end
19
+
20
+ def caffeinate_subscribe_path(subscription, **options)
21
+ Caffeinate::Engine.routes.url_helpers.subscribe_campaign_subscription_path(token: subscription.token, **options)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'caffeinate/helpers'
4
+
5
+ module Caffeinate
6
+ # Convenience class for the URL helpers.
7
+ class UrlHelpers
8
+ extend Helpers
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.1'
5
5
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Generators
5
- # :nodoc:
5
+ # Installs Caffeinate
6
6
  class InstallGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path('templates', __dir__)
8
8
  include ::Rails::Generators::Migration
@@ -19,6 +19,10 @@ module Caffeinate
19
19
  template 'application_dripper.rb', 'app/drippers/application_dripper.rb'
20
20
  end
21
21
 
22
+ def install_routes
23
+ inject_into_file 'config/routes.rb', "\n mount ::Caffeinate::Engine => '/caffeinate", after: /Rails.application.routes.draw do/
24
+ end
25
+
22
26
  # :nodoc:
23
27
  def self.next_migration_number(_path)
24
28
  if @prev_migration_nr
@@ -6,7 +6,7 @@ Caffeinate.setup do |config|
6
6
  # Used for when we set a datetime column to "now" in the database
7
7
  #
8
8
  # Default:
9
- # -> { Time.current }
9
+ # config.now = -> { Time.current }
10
10
  #
11
11
  # config.now = -> { DateTime.now }
12
12
  #
@@ -21,4 +21,14 @@ Caffeinate.setup do |config|
21
21
  #
22
22
  # config.async_delivery = true
23
23
  # config.mailing_job = 'MyCustomCaffeinateJob'
24
+ #
25
+ # == Batching
26
+ #
27
+ # When a Dripper is performed and sends the mails, we use `find_in_batches`. Use `batch_size` to set the batch size.
28
+ # You can set this on a dripper as well for more granular control.
29
+ #
30
+ # Default:
31
+ # config.batch_size = 1_000
32
+ #
33
+ # config.batch_size = 100
24
34
  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.1.1
4
+ version: 0.2.1
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-11-27 00:00:00.000000000 Z
11
+ date: 2020-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -124,8 +124,10 @@ files:
124
124
  - app/models/caffeinate/campaign.rb
125
125
  - app/models/caffeinate/campaign_subscription.rb
126
126
  - app/models/caffeinate/mailing.rb
127
- - app/views/layouts/caffeinate/application.html.erb
128
- - app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb
127
+ - app/views/caffeinate/campaign_subscriptions/subscribe.html.erb
128
+ - app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb
129
+ - app/views/layouts/caffeinate.html.erb
130
+ - config/locales/en.yml
129
131
  - config/routes.rb
130
132
  - db/migrate/20201124183102_create_caffeinate_campaigns.rb
131
133
  - db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb
@@ -133,25 +135,29 @@ files:
133
135
  - lib/caffeinate.rb
134
136
  - lib/caffeinate/action_mailer.rb
135
137
  - lib/caffeinate/action_mailer/extension.rb
136
- - lib/caffeinate/action_mailer/helpers.rb
137
138
  - lib/caffeinate/action_mailer/interceptor.rb
138
139
  - lib/caffeinate/action_mailer/observer.rb
139
140
  - lib/caffeinate/active_record/extension.rb
140
141
  - lib/caffeinate/configuration.rb
141
142
  - lib/caffeinate/deliver_async.rb
142
143
  - lib/caffeinate/drip.rb
144
+ - lib/caffeinate/drip_evaluator.rb
143
145
  - lib/caffeinate/dripper/base.rb
146
+ - lib/caffeinate/dripper/batching.rb
144
147
  - lib/caffeinate/dripper/callbacks.rb
145
148
  - lib/caffeinate/dripper/campaign.rb
146
149
  - lib/caffeinate/dripper/defaults.rb
147
150
  - lib/caffeinate/dripper/delivery.rb
148
151
  - lib/caffeinate/dripper/drip.rb
152
+ - lib/caffeinate/dripper/inferences.rb
149
153
  - lib/caffeinate/dripper/perform.rb
150
154
  - lib/caffeinate/dripper/subscriber.rb
151
155
  - lib/caffeinate/engine.rb
156
+ - lib/caffeinate/helpers.rb
157
+ - lib/caffeinate/url_helpers.rb
152
158
  - lib/caffeinate/version.rb
153
159
  - lib/generators/caffeinate/install_generator.rb
154
- - lib/generators/caffeinate/templates/application_campaign.rb
160
+ - lib/generators/caffeinate/templates/application_dripper.rb
155
161
  - lib/generators/caffeinate/templates/caffeinate.rb
156
162
  homepage: https://github.com/joshmn/caffeinate
157
163
  licenses:
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Caffeinate</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
7
-
8
- <%= stylesheet_link_tag "caffeinate/application", media: "all" %>
9
- </head>
10
- <body>
11
-
12
- <%= yield %>
13
-
14
- </body>
15
- </html>
@@ -1 +0,0 @@
1
- <h5>You have been unsubscribed.</h5>
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Caffeinate
4
- module ActionMailer
5
- module Helpers
6
- def caffeinate_unsubscribe_url(caffeinate_campaign_subscription, **options)
7
- opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
8
- Caffeinate::Engine.routes.url_helpers.caffeinate_unsubscribe_url(token: caffeinate_campaign_subscription.token, **opts)
9
- end
10
- end
11
- end
12
- end