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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE +661 -0
  4. data/README.md +573 -0
  5. data/Rakefile +14 -14
  6. data/app/mailers/heya/application_mailer.rb +0 -2
  7. data/app/mailers/heya/campaign_mailer.rb +52 -0
  8. data/app/models/heya/campaign_membership.rb +90 -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 +69 -0
  14. data/lib/generators/heya/campaign/templates/campaign.rb.tt +4 -0
  15. data/lib/generators/heya/campaign/templates/message.html.erb.tt +1 -0
  16. data/lib/generators/heya/campaign/templates/message.md.erb.tt +1 -0
  17. data/lib/generators/heya/campaign/templates/message.text.erb.tt +1 -0
  18. data/lib/generators/heya/campaign/templates/preview.rb.tt +12 -0
  19. data/lib/generators/heya/install/USAGE +10 -0
  20. data/lib/generators/heya/install/install_generator.rb +24 -0
  21. data/lib/generators/heya/install/templates/application_campaign.rb.tt +3 -0
  22. data/lib/generators/heya/install/templates/initializer.rb.tt +16 -0
  23. data/lib/generators/heya/install/templates/migration.rb.tt +29 -0
  24. data/lib/heya.rb +45 -1
  25. data/lib/heya/active_record_extension.rb +37 -0
  26. data/lib/heya/campaigns/action.rb +27 -0
  27. data/lib/heya/campaigns/actions/block.rb +24 -0
  28. data/lib/heya/campaigns/actions/email.rb +24 -0
  29. data/lib/heya/campaigns/base.rb +154 -0
  30. data/lib/heya/campaigns/queries.rb +35 -0
  31. data/lib/heya/campaigns/scheduler.rb +50 -0
  32. data/lib/heya/campaigns/step.rb +35 -0
  33. data/lib/heya/campaigns/step_action_job.rb +34 -0
  34. data/lib/heya/config.rb +17 -0
  35. data/lib/heya/engine.rb +14 -0
  36. data/lib/heya/version.rb +3 -1
  37. data/lib/tasks/heya_tasks.rake +8 -4
  38. metadata +64 -11
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,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,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 @@
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,4 @@
1
+ class <%= file_name.camelcase %>Campaign < ApplicationCampaign<% steps.each do |step| %><% step, wait = step.split(":") %>
2
+ step :<%= step.underscore.to_sym %>,<%= wait.presence ? "\n wait: #{wait}," : "" %>
3
+ subject: "<%= step.humanize %>"
4
+ <% end %>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,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,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,3 @@
1
+ class ApplicationCampaign < Heya::Campaigns::Base
2
+ default from: "from@example.com"
3
+ 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
- # Your code goes here...
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