heya 0.0.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/LICENSE +661 -0
- data/README.md +573 -0
- data/Rakefile +14 -14
- data/app/mailers/heya/application_mailer.rb +0 -2
- data/app/mailers/heya/campaign_mailer.rb +52 -0
- data/app/models/heya/campaign_membership.rb +90 -0
- data/app/models/heya/campaign_receipt.rb +5 -0
- data/app/views/layouts/heya/campaign_mailer.html.erb +20 -0
- data/app/views/layouts/heya/campaign_mailer.text.erb +1 -0
- data/lib/generators/heya/campaign/USAGE +14 -0
- data/lib/generators/heya/campaign/campaign_generator.rb +69 -0
- data/lib/generators/heya/campaign/templates/campaign.rb.tt +4 -0
- data/lib/generators/heya/campaign/templates/message.html.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/message.md.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/message.text.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/preview.rb.tt +12 -0
- data/lib/generators/heya/install/USAGE +10 -0
- data/lib/generators/heya/install/install_generator.rb +24 -0
- data/lib/generators/heya/install/templates/application_campaign.rb.tt +3 -0
- data/lib/generators/heya/install/templates/initializer.rb.tt +16 -0
- data/lib/generators/heya/install/templates/migration.rb.tt +29 -0
- data/lib/heya.rb +45 -1
- data/lib/heya/active_record_extension.rb +37 -0
- data/lib/heya/campaigns/action.rb +27 -0
- data/lib/heya/campaigns/actions/block.rb +24 -0
- data/lib/heya/campaigns/actions/email.rb +24 -0
- data/lib/heya/campaigns/base.rb +154 -0
- data/lib/heya/campaigns/queries.rb +35 -0
- data/lib/heya/campaigns/scheduler.rb +50 -0
- data/lib/heya/campaigns/step.rb +35 -0
- data/lib/heya/campaigns/step_action_job.rb +34 -0
- data/lib/heya/config.rb +17 -0
- data/lib/heya/engine.rb +14 -0
- data/lib/heya/version.rb +3 -1
- data/lib/tasks/heya_tasks.rake +8 -4
- metadata +64 -11
data/Rakefile
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
begin
|
2
|
-
require
|
2
|
+
require "bundler/setup"
|
3
3
|
rescue LoadError
|
4
|
-
puts
|
4
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
5
5
|
end
|
6
6
|
|
7
|
-
require
|
7
|
+
require "rdoc/task"
|
8
8
|
|
9
9
|
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
-
rdoc.rdoc_dir =
|
11
|
-
rdoc.title
|
12
|
-
rdoc.options <<
|
13
|
-
rdoc.rdoc_files.include(
|
14
|
-
rdoc.rdoc_files.include(
|
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
|
18
|
+
load "rails/tasks/engine.rake"
|
19
19
|
|
20
|
-
load
|
20
|
+
load "rails/tasks/statistics.rake"
|
21
21
|
|
22
|
-
require
|
22
|
+
require "bundler/gem_tasks"
|
23
23
|
|
24
|
-
require
|
24
|
+
require "rake/testtask"
|
25
25
|
|
26
26
|
Rake::TestTask.new(:test) do |t|
|
27
|
-
t.libs <<
|
28
|
-
t.pattern =
|
27
|
+
t.libs << "test"
|
28
|
+
t.pattern = "test/**/*_test.rb"
|
29
29
|
t.verbose = false
|
30
30
|
end
|
31
31
|
|
@@ -0,0 +1,52 @@
|
|
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
|
+
|
9
|
+
campaign_name = step.campaign_name.underscore
|
10
|
+
step_name = step.name.underscore
|
11
|
+
|
12
|
+
from = step.params.fetch("from")
|
13
|
+
reply_to = step.params.fetch("reply_to", nil)
|
14
|
+
|
15
|
+
subject = step.params.fetch("subject") {
|
16
|
+
I18n.t("#{campaign_name}.#{step_name}.subject", **attributes_for(user))
|
17
|
+
}
|
18
|
+
subject = subject.call(user) if subject.respond_to?(:call)
|
19
|
+
|
20
|
+
instance_variable_set(:"@#{user.model_name.element}", user)
|
21
|
+
|
22
|
+
mail(
|
23
|
+
from: from,
|
24
|
+
reply_to: reply_to,
|
25
|
+
to: user.email,
|
26
|
+
subject: subject,
|
27
|
+
template_path: "heya/campaign_mailer/#{campaign_name}",
|
28
|
+
template_name: step_name
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def attributes_for(user)
|
35
|
+
if user.respond_to?(:heya_attributes)
|
36
|
+
user.heya_attributes.symbolize_keys
|
37
|
+
else
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def _prefixes
|
43
|
+
@_prefixes_with_campaign_path ||= begin
|
44
|
+
if params.is_a?(Hash) && (campaign_name = params[:step]&.campaign&.name&.underscore)
|
45
|
+
super | ["heya/campaign_mailer/#{campaign_name}"]
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Heya
|
4
|
+
class CampaignMembership < ApplicationRecord
|
5
|
+
belongs_to :user, polymorphic: true
|
6
|
+
|
7
|
+
before_create do
|
8
|
+
self.last_sent_at = Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
scope :with_steps, -> {
|
12
|
+
joins(
|
13
|
+
%(INNER JOIN "heya_steps" ON "heya_steps".gid = "heya_campaign_memberships".step_gid)
|
14
|
+
)
|
15
|
+
}
|
16
|
+
|
17
|
+
scope :active, -> {
|
18
|
+
priority_gids = Heya.config.campaigns.priority.map { |c| (c.is_a?(String) ? c.constantize : c).gid }
|
19
|
+
where(<<~SQL, priority_gids: priority_gids)
|
20
|
+
"heya_campaign_memberships".concurrent = TRUE
|
21
|
+
OR "heya_campaign_memberships"."campaign_gid" IN (
|
22
|
+
SELECT
|
23
|
+
"active_membership"."campaign_gid"
|
24
|
+
FROM
|
25
|
+
"heya_campaign_memberships" as "active_membership"
|
26
|
+
WHERE
|
27
|
+
"active_membership"."concurrent" = FALSE
|
28
|
+
AND
|
29
|
+
(
|
30
|
+
"active_membership".user_type = "heya_campaign_memberships".user_type
|
31
|
+
AND
|
32
|
+
"active_membership".user_id = "heya_campaign_memberships".user_id
|
33
|
+
)
|
34
|
+
ORDER BY
|
35
|
+
array_position(ARRAY[:priority_gids], "active_membership".campaign_gid::text) ASC,
|
36
|
+
"active_membership".created_at ASC
|
37
|
+
LIMIT 1
|
38
|
+
)
|
39
|
+
SQL
|
40
|
+
}
|
41
|
+
|
42
|
+
scope :upcoming, -> {
|
43
|
+
with_steps
|
44
|
+
.active
|
45
|
+
.order(
|
46
|
+
Arel.sql(
|
47
|
+
%("heya_campaign_memberships".last_sent_at + make_interval(secs := "heya_steps".wait) DESC)
|
48
|
+
)
|
49
|
+
)
|
50
|
+
}
|
51
|
+
|
52
|
+
scope :to_process, ->(now: Time.now, user: nil) {
|
53
|
+
upcoming
|
54
|
+
.where(<<~SQL, now: now.utc, user_type: user&.class&.base_class&.name, user_id: user&.id)
|
55
|
+
("heya_campaign_memberships".last_sent_at <= (TIMESTAMP :now - make_interval(secs := "heya_steps".wait)))
|
56
|
+
AND (
|
57
|
+
(:user_type IS NULL OR :user_id IS NULL)
|
58
|
+
OR (
|
59
|
+
"heya_campaign_memberships".user_type = :user_type
|
60
|
+
AND
|
61
|
+
"heya_campaign_memberships".user_id = :user_id
|
62
|
+
)
|
63
|
+
)
|
64
|
+
SQL
|
65
|
+
}
|
66
|
+
|
67
|
+
def self.migrate_next_step!
|
68
|
+
find_each do |membership|
|
69
|
+
campaign = GlobalID::Locator.locate(membership.campaign_gid)
|
70
|
+
receipt = campaign && CampaignReceipt.where(user: membership.user, step_gid: campaign.steps.map(&:gid)).order("created_at desc").first
|
71
|
+
|
72
|
+
next_step = if receipt
|
73
|
+
last_step = GlobalID::Locator.locate(receipt.step_gid)
|
74
|
+
current_index = campaign.steps.index(last_step)
|
75
|
+
campaign.steps[current_index + 1]
|
76
|
+
else
|
77
|
+
campaign&.steps&.first
|
78
|
+
end
|
79
|
+
|
80
|
+
if next_step
|
81
|
+
membership.update(step_gid: next_step.gid)
|
82
|
+
else
|
83
|
+
membership.destroy
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
CampaignReceipt.where(sent_at: nil).destroy_all
|
88
|
+
end
|
89
|
+
end
|
90
|
+
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 @@
|
|
1
|
+
<%= yield %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Description:
|
2
|
+
Generate a new campaign
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate heya:campaign Onboarding first:0 second:2.days 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,69 @@
|
|
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
|
+
application_campaign = "app/campaigns/application_campaign.rb"
|
10
|
+
unless File.exist?(application_campaign)
|
11
|
+
template File.expand_path("../install/templates/application_campaign.rb", __dir__), application_campaign
|
12
|
+
end
|
13
|
+
template "campaign.rb", "app/campaigns/#{file_name.underscore}_campaign.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def copy_view_templates
|
17
|
+
selection =
|
18
|
+
if defined?(Maildown)
|
19
|
+
puts <<~MSG
|
20
|
+
What type of views would you like to generate?
|
21
|
+
1. Multipart (text/html)
|
22
|
+
2. Maildown (markdown)
|
23
|
+
MSG
|
24
|
+
|
25
|
+
ask(">")
|
26
|
+
else
|
27
|
+
"1"
|
28
|
+
end
|
29
|
+
|
30
|
+
template_method =
|
31
|
+
case selection
|
32
|
+
when "1"
|
33
|
+
method(:action_mailer_template)
|
34
|
+
when "2"
|
35
|
+
method(:maildown_template)
|
36
|
+
else
|
37
|
+
abort "Error: must be a number [1-2]"
|
38
|
+
end
|
39
|
+
|
40
|
+
steps.each do |step|
|
41
|
+
step, _wait = step.split(":")
|
42
|
+
@step = step
|
43
|
+
template_method.call(step)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def copy_test_templates
|
48
|
+
if preview_path
|
49
|
+
template "preview.rb", preview_path.join("#{file_name.underscore}_campaign_preview.rb")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def action_mailer_template(step)
|
56
|
+
template "message.text.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.text.erb"
|
57
|
+
template "message.html.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.html.erb"
|
58
|
+
end
|
59
|
+
|
60
|
+
def maildown_template(step)
|
61
|
+
template "message.md.erb", "app/views/heya/campaign_mailer/#{file_name.underscore}_campaign/#{step.underscore.to_sym}.md.erb"
|
62
|
+
end
|
63
|
+
|
64
|
+
def preview_path
|
65
|
+
@preview_path ||= if ActionMailer::Base.preview_path.present?
|
66
|
+
Pathname(ActionMailer::Base.preview_path).sub(Rails.root.to_s, ".")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
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,12 @@
|
|
1
|
+
# Preview all emails at http://localhost:3000/rails/mailers/
|
2
|
+
class <%= file_name.camelcase %>CampaignPreview < ActionMailer::Preview<% steps.each do |step| %><% step, wait = step.split(":") %>
|
3
|
+
def <%= step %>
|
4
|
+
<%= file_name.camelcase %>Campaign.<%= step %>(user)
|
5
|
+
end
|
6
|
+
<% end %>
|
7
|
+
private
|
8
|
+
|
9
|
+
def user
|
10
|
+
<%= Heya.config.user_type %>.where(id: params[:user_id]).first || <%= Heya.config.user_type %>.first || <%= Heya.config.user_type %>.new(email: "user@example.com").freeze
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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 copy_application_campaign_template
|
17
|
+
template "application_campaign.rb", "app/campaigns/application_campaign.rb"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.next_migration_number(dirname)
|
21
|
+
next_migration_number = current_migration_number(dirname) + 1
|
22
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
23
|
+
end
|
24
|
+
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,29 @@
|
|
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.string :step_gid, null: false
|
8
|
+
t.boolean :concurrent, null: false, default: false
|
9
|
+
|
10
|
+
t.datetime :last_sent_at, null: false
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :heya_campaign_memberships, [:user_type, :user_id, :campaign_gid], unique: true, name: :user_campaign_idx
|
16
|
+
|
17
|
+
create_table :heya_campaign_receipts do |t|
|
18
|
+
t.references :user, null: false, polymorphic: true, index: false
|
19
|
+
|
20
|
+
t.string :step_gid, null: false
|
21
|
+
|
22
|
+
t.datetime :sent_at
|
23
|
+
|
24
|
+
t.timestamps
|
25
|
+
end
|
26
|
+
|
27
|
+
add_index :heya_campaign_receipts, [:user_type, :user_id, :step_gid], unique: true, name: :user_step_idx
|
28
|
+
end
|
29
|
+
end
|
data/lib/heya.rb
CHANGED
@@ -1,5 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "heya/version"
|
4
|
+
require "heya/active_record_extension"
|
1
5
|
require "heya/engine"
|
6
|
+
require "heya/config"
|
7
|
+
require "heya/campaigns/action"
|
8
|
+
require "heya/campaigns/actions/email"
|
9
|
+
require "heya/campaigns/actions/block"
|
10
|
+
require "heya/campaigns/base"
|
11
|
+
require "heya/campaigns/queries"
|
12
|
+
require "heya/campaigns/scheduler"
|
13
|
+
require "heya/campaigns/step"
|
14
|
+
require "heya/campaigns/step_action_job"
|
2
15
|
|
3
16
|
module Heya
|
4
|
-
|
17
|
+
extend self
|
18
|
+
|
19
|
+
attr_accessor :campaigns
|
20
|
+
self.campaigns = []
|
21
|
+
|
22
|
+
def register_campaign(klass)
|
23
|
+
campaigns.push(klass) unless campaigns.include?(klass)
|
24
|
+
end
|
25
|
+
|
26
|
+
def unregister_campaign(klass)
|
27
|
+
campaigns.delete(klass)
|
28
|
+
end
|
29
|
+
|
30
|
+
def configure
|
31
|
+
yield(config) if block_given?
|
32
|
+
config
|
33
|
+
end
|
34
|
+
|
35
|
+
def config
|
36
|
+
@config ||= Config.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def in_segments?(user, *segments)
|
40
|
+
return false if segments.any? { |s| !in_segment?(user, s) }
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def in_segment?(user, segment)
|
45
|
+
return true if segment.nil?
|
46
|
+
return user.send(segment) if segment.is_a?(Symbol)
|
47
|
+
segment.call(user)
|
48
|
+
end
|
5
49
|
end
|