caffeinate 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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