heya 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +85 -0
  4. data/README.md +418 -0
  5. data/Rakefile +14 -14
  6. data/app/mailers/heya/application_mailer.rb +0 -2
  7. data/app/mailers/heya/campaign_mailer.rb +25 -0
  8. data/app/models/heya/campaign_membership.rb +9 -0
  9. data/app/models/heya/campaign_receipt.rb +5 -0
  10. data/app/views/layouts/heya/campaign_mailer.html.erb +20 -0
  11. data/app/views/layouts/heya/campaign_mailer.text.erb +1 -0
  12. data/lib/generators/heya/campaign/USAGE +14 -0
  13. data/lib/generators/heya/campaign/campaign_generator.rb +53 -0
  14. data/lib/generators/heya/campaign/templates/application_campaign.rb.tt +3 -0
  15. data/lib/generators/heya/campaign/templates/campaign.rb.tt +7 -0
  16. data/lib/generators/heya/campaign/templates/message.html.erb.tt +1 -0
  17. data/lib/generators/heya/campaign/templates/message.md.erb.tt +1 -0
  18. data/lib/generators/heya/campaign/templates/message.text.erb.tt +1 -0
  19. data/lib/generators/heya/install/USAGE +10 -0
  20. data/lib/generators/heya/install/install_generator.rb +20 -0
  21. data/lib/generators/heya/install/templates/initializer.rb.tt +16 -0
  22. data/lib/generators/heya/install/templates/migration.rb.tt +28 -0
  23. data/lib/heya.rb +44 -1
  24. data/lib/heya/campaigns/action.rb +27 -0
  25. data/lib/heya/campaigns/actions/block.rb +24 -0
  26. data/lib/heya/campaigns/actions/email.rb +15 -0
  27. data/lib/heya/campaigns/base.rb +151 -0
  28. data/lib/heya/campaigns/queries.rb +118 -0
  29. data/lib/heya/campaigns/scheduler.rb +49 -0
  30. data/lib/heya/campaigns/step.rb +24 -0
  31. data/lib/heya/campaigns/step_action_job.rb +34 -0
  32. data/lib/heya/config.rb +17 -0
  33. data/lib/heya/engine.rb +14 -0
  34. data/lib/heya/version.rb +3 -1
  35. data/lib/tasks/heya_tasks.rake +8 -4
  36. metadata +58 -7
data/Rakefile CHANGED
@@ -1,31 +1,31 @@
1
1
  begin
2
- require 'bundler/setup'
2
+ require "bundler/setup"
3
3
  rescue LoadError
4
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
5
  end
6
6
 
7
- require 'rdoc/task'
7
+ require "rdoc/task"
8
8
 
9
9
  RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'Heya'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.md')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "Heya"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.md")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
15
  end
16
16
 
17
17
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
- load 'rails/tasks/engine.rake'
18
+ load "rails/tasks/engine.rake"
19
19
 
20
- load 'rails/tasks/statistics.rake'
20
+ load "rails/tasks/statistics.rake"
21
21
 
22
- require 'bundler/gem_tasks'
22
+ require "bundler/gem_tasks"
23
23
 
24
- require 'rake/testtask'
24
+ require "rake/testtask"
25
25
 
26
26
  Rake::TestTask.new(:test) do |t|
27
- t.libs << 'test'
28
- t.pattern = 'test/**/*_test.rb'
27
+ t.libs << "test"
28
+ t.pattern = "test/**/*_test.rb"
29
29
  t.verbose = false
30
30
  end
31
31
 
@@ -1,6 +1,4 @@
1
1
  module Heya
2
2
  class ApplicationMailer < ActionMailer::Base
3
- default from: 'from@example.com'
4
- layout 'mailer'
5
3
  end
6
4
  end
@@ -0,0 +1,25 @@
1
+ module Heya
2
+ class CampaignMailer < ApplicationMailer
3
+ layout "heya/campaign_mailer"
4
+
5
+ def build
6
+ user = params.fetch(:user)
7
+ step = params.fetch(:step)
8
+ campaign = step.campaign
9
+ from = step.params.fetch("from")
10
+ reply_to = step.params.fetch("reply_to", nil)
11
+ subject = step.params.fetch("subject")
12
+
13
+ instance_variable_set(:"@#{user.model_name.element}", user)
14
+
15
+ mail(
16
+ from: from,
17
+ reply_to: reply_to,
18
+ to: user.email,
19
+ subject: subject,
20
+ template_path: "heya/campaign_mailer/#{campaign.name.underscore}",
21
+ template_name: step.name.underscore
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Heya
2
+ class CampaignMembership < ApplicationRecord
3
+ belongs_to :user, polymorphic: true
4
+
5
+ before_create do
6
+ self.last_sent_at = Time.now
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Heya
2
+ class CampaignReceipt < ApplicationRecord
3
+ belongs_to :user, polymorphic: true
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ .message-content { max-width: 600px; }
7
+ .message-content div { padding-bottom: 10px; }
8
+ .message-content img { max-width: 100%; height: auto; }
9
+ p {
10
+ margin-bottom: 1em;
11
+ }
12
+ </style>
13
+ </head>
14
+
15
+ <body>
16
+ <div class="message-content">
17
+ <%= yield %>
18
+ </div>
19
+ </body>
20
+ </html>
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Generate a new campaign
3
+
4
+ Example:
5
+ rails generate heya:campaign Onboarding first second third
6
+
7
+ This will create:
8
+ app/campaigns/onboarding_campaign.rb
9
+ app/views/heya/campaign_mailer/onboarding_campaign/first.text.erb
10
+ app/views/heya/campaign_mailer/onboarding_campaign/first.html.erb
11
+ app/views/heya/campaign_mailer/onboarding_campaign/second.text.erb
12
+ app/views/heya/campaign_mailer/onboarding_campaign/second.html.erb
13
+ app/views/heya/campaign_mailer/onboarding_campaign/third.text.erb
14
+ app/views/heya/campaign_mailer/onboarding_campaign/third.html.erb
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Heya::CampaignGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ argument :steps, type: :array, default: []
7
+
8
+ def copy_campaign_template
9
+ template "application_campaign.rb", "app/campaigns/application_campaign.rb"
10
+ template "campaign.rb", "app/campaigns/#{file_name.underscore}_campaign.rb"
11
+ end
12
+
13
+ def copy_view_templates
14
+ selection =
15
+ if defined?(Maildown)
16
+ puts <<~MSG
17
+ What type of views would you like to generate?
18
+ 1. Multipart (text/html)
19
+ 2. Maildown (markdown)
20
+ MSG
21
+
22
+ ask(">")
23
+ else
24
+ "1"
25
+ end
26
+
27
+ template_method =
28
+ case selection
29
+ when "1"
30
+ method(:action_mailer_template)
31
+ when "2"
32
+ method(:maildown_template)
33
+ else
34
+ abort "Error: must be a number [1-2]"
35
+ end
36
+
37
+ steps.each do |step|
38
+ @step = step
39
+ template_method.call(step)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def action_mailer_template(step)
46
+ template "message.text.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.text.erb"
47
+ template "message.html.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.html.erb"
48
+ end
49
+
50
+ def maildown_template(step)
51
+ template "message.md.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.md.erb"
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationCampaign < Heya::Campaigns::Base
2
+ default from: "from@example.com"
3
+ end
@@ -0,0 +1,7 @@
1
+ class <%= file_name.camelcase %>Campaign < ApplicationCampaign
2
+ <% steps.each do |step| %>
3
+ step :<%= step.underscore.to_sym %>,
4
+ subject: "<%= step.humanize %> Message"
5
+
6
+ <% end -%>
7
+ end
@@ -0,0 +1 @@
1
+ This is the <%= @step.downcase %> messsage.
@@ -0,0 +1 @@
1
+ This is the <%= @step.downcase %> messsage.
@@ -0,0 +1 @@
1
+ This is the <%= @step.downcase %> messsage.
@@ -0,0 +1,10 @@
1
+ Description:
2
+ Install Heya initializer and migrations
3
+
4
+ Example:
5
+ rails generate heya:install
6
+
7
+ This will create:
8
+ db/migrate/*_create_heya_campaign_receipts.heya.rb
9
+ db/migrate/*_create_heya_campaign_memberships.heya.rb
10
+ config/initializers/heya.rb
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Heya::InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_migrations
9
+ migration_template "migration.rb", "db/migrate/create_heya_tables.rb"
10
+ end
11
+
12
+ def copy_initializer_file
13
+ copy_file "initializer.rb", "config/initializers/heya.rb"
14
+ end
15
+
16
+ def self.next_migration_number(dirname)
17
+ next_migration_number = current_migration_number(dirname) + 1
18
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ Heya.configure do |config|
2
+ # The name of the model you want to use with Heya.
3
+ config.user_type = "User"
4
+
5
+ # The default options to use when processing campaign steps.
6
+ # config.campaigns.default_options = {from: "user@example.com"}
7
+
8
+ # Campaign priority. When a user is added to multiple campaigns, they are
9
+ # sent in this order. Campaigns are sent in the order that the users were
10
+ # added if no priority is configured.
11
+ # config.campaings.priority = [
12
+ # "FirstCampaign",
13
+ # "SecondCampaign",
14
+ # "ThirdCampaign"
15
+ # ]
16
+ end
@@ -0,0 +1,28 @@
1
+ class CreateHeyaTables < ActiveRecord::Migration[<%= ActiveRecord::VERSION::MAJOR %>.<%= ActiveRecord::VERSION::MINOR %>]
2
+ def change
3
+ create_table :heya_campaign_memberships do |t|
4
+ t.references :user, null: false, polymorphic: true, index: false
5
+
6
+ t.string :campaign_gid, null: false
7
+ t.boolean :concurrent, null: false, default: false
8
+
9
+ t.datetime :last_sent_at, null: false
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :heya_campaign_memberships, [:user_type, :user_id, :campaign_gid], unique: true, name: :user_campaign_idx
15
+
16
+ create_table :heya_campaign_receipts do |t|
17
+ t.references :user, null: false, polymorphic: true, index: false
18
+
19
+ t.string :step_gid, null: false
20
+
21
+ t.datetime :sent_at
22
+
23
+ t.timestamps
24
+ end
25
+
26
+ add_index :heya_campaign_receipts, [:user_type, :user_id, :step_gid], unique: true, name: :user_step_idx
27
+ end
28
+ end
@@ -1,5 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "heya/version"
1
4
  require "heya/engine"
5
+ require "heya/config"
6
+ require "heya/campaigns/action"
7
+ require "heya/campaigns/actions/email"
8
+ require "heya/campaigns/actions/block"
9
+ require "heya/campaigns/base"
10
+ require "heya/campaigns/queries"
11
+ require "heya/campaigns/scheduler"
12
+ require "heya/campaigns/step"
13
+ require "heya/campaigns/step_action_job"
2
14
 
3
15
  module Heya
4
- # Your code goes here...
16
+ extend self
17
+
18
+ attr_accessor :campaigns
19
+ self.campaigns = []
20
+
21
+ def register_campaign(klass)
22
+ campaigns.push(klass) unless campaigns.include?(klass)
23
+ end
24
+
25
+ def unregister_campaign(klass)
26
+ campaigns.delete(klass)
27
+ end
28
+
29
+ def configure
30
+ yield(config) if block_given?
31
+ config
32
+ end
33
+
34
+ def config
35
+ @config ||= Config.new
36
+ end
37
+
38
+ def in_segments?(user, *segments)
39
+ return false if segments.any? { |s| !in_segment?(user, s) }
40
+ true
41
+ end
42
+
43
+ def in_segment?(user, segment)
44
+ return true if segment.nil?
45
+ return user.send(segment) if segment.is_a?(Symbol)
46
+ segment.call(user)
47
+ end
5
48
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heya
4
+ module Campaigns
5
+ class Action
6
+ def initialize(user:, step:)
7
+ @user, @step = user, step
8
+ end
9
+
10
+ attr_reader :user, :step
11
+
12
+ def build
13
+ raise NotImplementedError, "Please implement #build on subclass of Heya::Campaigns::Action."
14
+ end
15
+
16
+ def deliver_now
17
+ build.deliver
18
+ end
19
+
20
+ def deliver_later
21
+ StepActionJob
22
+ .set(queue: step.queue)
23
+ .perform_later(step.campaign.class.name, user, step)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heya
4
+ module Campaigns
5
+ module Actions
6
+ class Block < Action
7
+ class Execution
8
+ def initialize(user:, step:, &block)
9
+ @user, @step, @block = user, step, block
10
+ end
11
+
12
+ def deliver
13
+ instance_exec(@user, @step, &@block)
14
+ end
15
+ end
16
+
17
+ def build
18
+ block = step.params.fetch("block")
19
+ Execution.new(user: user, step: step, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heya
4
+ module Campaigns
5
+ module Actions
6
+ class Email < Action
7
+ def build
8
+ CampaignMailer
9
+ .with(user: user, step: step)
10
+ .build
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/descendants_tracker"
4
+
5
+ module Heya
6
+ module Campaigns
7
+ # {Campaigns::Base} provides a Ruby DSL for building campaign sequences.
8
+ # Multiple actions are supported; the default is email.
9
+ class Base
10
+ extend ActiveSupport::DescendantsTracker
11
+
12
+ include Singleton
13
+ include GlobalID::Identification
14
+
15
+ def initialize
16
+ self.steps = []
17
+ end
18
+
19
+ delegate :name, :__segments, to: :class
20
+ alias id name
21
+
22
+ # Returns String GlobalID.
23
+ def gid
24
+ to_gid(app: "heya").to_s
25
+ end
26
+
27
+ def add(user, restart: false, concurrent: false, send_now: true)
28
+ return false unless Heya.in_segments?(user, *__segments)
29
+
30
+ restart && CampaignReceipt
31
+ .where(user: user, step_gid: steps.map(&:gid))
32
+ .delete_all
33
+
34
+ CampaignMembership.where(user: user, campaign_gid: gid, concurrent: concurrent).first_or_create!
35
+
36
+ if send_now && (step = steps.first) && step.wait <= 0
37
+ Scheduler.process(self, step, user)
38
+ end
39
+
40
+ true
41
+ end
42
+
43
+ def remove(user)
44
+ CampaignMembership.where(user: user, campaign_gid: gid).delete_all
45
+ true
46
+ end
47
+
48
+ def users
49
+ base_class = user_class.base_class
50
+ user_class
51
+ .joins(
52
+ sanitize_sql_array([
53
+ "inner join heya_campaign_memberships on heya_campaign_memberships.user_type = ? and heya_campaign_memberships.user_id = #{base_class.table_name}.id and heya_campaign_memberships.campaign_gid = ?",
54
+ base_class.name,
55
+ gid
56
+ ])
57
+ ).all
58
+ end
59
+
60
+ def user_class
61
+ @user_class ||= self.class.user_type.constantize
62
+ end
63
+
64
+ attr_accessor :steps
65
+
66
+ private
67
+
68
+ delegate :sanitize_sql_array, to: ActiveRecord::Base
69
+
70
+ class_attribute :__defaults, default: {}.freeze
71
+ class_attribute :__segments, default: [].freeze
72
+ class_attribute :__user_type, default: nil
73
+
74
+ STEP_ATTRS = {
75
+ action: Actions::Email,
76
+ wait: 2.days,
77
+ segment: nil,
78
+ queue: "heya"
79
+ }.freeze
80
+
81
+ class << self
82
+ def inherited(campaign)
83
+ Heya.register_campaign(campaign)
84
+ Heya.unregister_campaign(campaign.superclass)
85
+ super
86
+ end
87
+
88
+ def find(_id)
89
+ instance
90
+ end
91
+
92
+ def handle_exception(exception)
93
+ raise exception
94
+ end
95
+
96
+ delegate :steps, :add, :remove, :users, :gid, :user_class, to: :instance
97
+
98
+ def default(**params)
99
+ self.__defaults = __defaults.merge(params).freeze
100
+ end
101
+
102
+ def user_type(value = nil)
103
+ if value.present?
104
+ self.__user_type = value.is_a?(String) ? value.to_s : value.name
105
+ end
106
+
107
+ __user_type || Heya.config.user_type
108
+ end
109
+
110
+ def segment(arg = nil, &block)
111
+ if block_given?
112
+ self.__segments = ([block] | __segments).freeze
113
+ elsif arg
114
+ self.__segments = ([arg] | __segments).freeze
115
+ end
116
+ end
117
+
118
+ def step(name, **opts, &block)
119
+ if block_given?
120
+ opts[:block] ||= block
121
+ opts[:action] ||= Actions::Block
122
+ end
123
+
124
+ opts =
125
+ STEP_ATTRS
126
+ .merge(Heya.config.campaigns.default_options)
127
+ .merge(__defaults)
128
+ .merge(opts)
129
+
130
+ attrs = opts.select { |k, _| STEP_ATTRS.key?(k) }
131
+ attrs[:params] = opts.reject { |k, _| STEP_ATTRS.key?(k) }.stringify_keys
132
+ attrs[:id] = "#{self.name}/#{name}"
133
+ attrs[:name] = name.to_s
134
+ attrs[:position] = steps.size
135
+ attrs[:campaign] = instance
136
+
137
+ step = Step.new(attrs)
138
+ method_name = :"#{step.name.underscore}"
139
+ raise "Invalid step name: #{step.name}\n Step names must not conflict with method names on Heya::Campaigns::Base" if respond_to?(method_name)
140
+
141
+ define_singleton_method method_name do |user|
142
+ step.action.new(user: user, step: step).build
143
+ end
144
+ steps << step
145
+
146
+ step
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end