diff --git a/db/up/periodic_emails.sql b/db/up/periodic_emails.sql index bfdbb8716..b67864d84 100644 --- a/db/up/periodic_emails.sql +++ b/db/up/periodic_emails.sql @@ -1,2 +1,12 @@ ALTER TABLE email_batches ADD COLUMN type VARCHAR(64) NOT NULL DEFAULT 'JamRuby::EmailBatch'; ALTER TABLE email_batches ADD COLUMN sub_type VARCHAR(64); + +ALTER TABLE email_batches ALTER COLUMN body DROP NOT NULL; +ALTER TABLE email_batches ALTER COLUMN subject DROP NOT NULL; + +ALTER TABLE email_batch_sets ADD COLUMN trigger_index INTEGER NOT NULL DEFAULT 0; +ALTER TABLE email_batch_sets ADD COLUMN sub_type VARCHAR(64); +ALTER TABLE email_batch_sets ADD COLUMN user_id VARCHAR(64); + +CREATE INDEX email_batch_sets_progress_idx ON email_batch_sets(user_id, sub_type); +CREATE INDEX users_musician_email_idx ON users(subscribe_email, musician); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 2ea2b5a2b..9d74fc4ac 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -138,6 +138,9 @@ require "jam_ruby/models/country" require "jam_ruby/models/region" require "jam_ruby/models/city" require "jam_ruby/models/email_batch" +require "jam_ruby/models/email_batch_periodic" +require "jam_ruby/models/email_batch_new_musician" +require "jam_ruby/models/email_batch_progression" require "jam_ruby/models/email_batch_set" require "jam_ruby/models/email_error" require "jam_ruby/app/mailers/async_mailer" diff --git a/ruby/lib/jam_ruby/app/mailers/progress_mailer.rb b/ruby/lib/jam_ruby/app/mailers/progress_mailer.rb new file mode 100644 index 000000000..86ab65b36 --- /dev/null +++ b/ruby/lib/jam_ruby/app/mailers/progress_mailer.rb @@ -0,0 +1,23 @@ +module JamRuby + class ProgressMailer < ActionMailer::Base + include SendGrid + layout "user_mailer" + + sendgrid_category :use_subject_lines + sendgrid_unique_args :env => Environment.mode + default :from => UserMailer::DEFAULT_SENDER + + def progress_reminder(batch_set) + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + mail(:to => batch_set.user.email, + :subject => batch_set.subject, + :title => batch_set.title, + :template_name => batch_set.sub_type) do |format| + format.text + format.html + end + end + + end +end diff --git a/ruby/lib/jam_ruby/models/email_batch_new_musician.rb b/ruby/lib/jam_ruby/models/email_batch_new_musician.rb index 0bb1c1ec0..7c752cb47 100644 --- a/ruby/lib/jam_ruby/models/email_batch_new_musician.rb +++ b/ruby/lib/jam_ruby/models/email_batch_new_musician.rb @@ -1,7 +1,7 @@ module JamRuby class EmailBatchNewMusician < EmailBatchPeriodic - BATCH_SIZE = 3 + BATCH_SIZE = 500 SINCE_WEEKS = 2 @@ -12,12 +12,12 @@ module JamRuby "New musicians with good Internet connections to you have joined JamKazam!" end - def self.fetch_recipients(since=nil) + def fetch_recipients(since=nil) since ||= Time.now - SINCE_WEEKS.weeks User.geocoded_users .email_opt_in .where(['created_at < ?', since]) - .find_in_batches(batch_size: self::BATCH_SIZE) do |users| + .find_in_batches(batch_size: EmailBatchNewMusician::BATCH_SIZE) do |users| new_musicians = users.inject({}) do |hh, uu| if 0 < (nearby = uu.nearest_musicians).count hh[uu] = nearby @@ -31,7 +31,7 @@ module JamRuby def deliver_batch_sets! self.opt_in_count = 0 sent = 0 - self.class.fetch_recipients(self.time_since_last_batch(SINCE_WEEKS)) do |new_musicians| + self.fetch_recipients(self.time_since_last_batch(SINCE_WEEKS)) do |new_musicians| self.opt_in_count += new_musicians.count self.email_batch_sets << (bset = EmailBatchSet.load_set(self, new_musicians.keys.map(&:id))) new_musicians.each do |uu, nearby| diff --git a/ruby/lib/jam_ruby/models/email_batch_periodic.rb b/ruby/lib/jam_ruby/models/email_batch_periodic.rb index e72cdc10e..5b62b46c5 100644 --- a/ruby/lib/jam_ruby/models/email_batch_periodic.rb +++ b/ruby/lib/jam_ruby/models/email_batch_periodic.rb @@ -2,22 +2,25 @@ module JamRuby class EmailBatchPeriodic < EmailBatch self.abstract_class = true - def time_since_last_batch(default_weeks=2) - if previous = self.class + def time_since_last_batch_query + self.class .where(['created_at < ?', self.created_at]) .order('created_at DESC') .limit(1) - .first + end + + def time_since_last_batch(default_weeks=2) + if previous = self.time_since_last_batch_query.first return previous.created_at end Time.now - default_weeks.weeks end - def self.fetch_recipients(since=nil) + def fetch_recipients(since=nil) yield([]) if block_given? end - def self.subject + def self.subject(subtype=nil) '' end @@ -27,7 +30,7 @@ module JamRuby def self.new(*args) oo = super - oo.body = nil + oo.body = '' oo.subject = self.subject oo end diff --git a/ruby/lib/jam_ruby/models/email_batch_progression.rb b/ruby/lib/jam_ruby/models/email_batch_progression.rb new file mode 100644 index 000000000..a989cc8dd --- /dev/null +++ b/ruby/lib/jam_ruby/models/email_batch_progression.rb @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +module JamRuby + class EmailBatchProgression < EmailBatchPeriodic + + BATCH_SIZE = 3 + SINCE_WEEKS = 2 + + SUBTYPES = [:client_notdl, # Registered Musician Has Not Downloaded Client + :client_dl_notrun, # Registered Musician Has Downloaded Client But Not Yet Run It + :client_run_notgear, # Registered Musician Has Run Client But Not Successfully Qualified Audio Gear + :gear_notsess, # Registered Musician Has Successfully Qualified Audio Gear But Has Not Participated in a ‘Real’ Session + :sess_notgood, # Registered Musician Has Participated In a "Real" Session But Has Not Had a "Good" Session + :reg_notinvite, # Registered Musician Has Not Invited Friends to Join JamKazam + :reg_notconnect, # Registered Musician Has Not Connected with any Friends on JamKazam + :reg_notlike, # Registered Musician Has Not Liked Jamkazam + :sess_notrecord # Registered Musician Has Participated In a "Real" Session But Has Not Made a Recording + ] + + SUBTYPE_METADATA = { + :client_notdl => { + :subject => "Get the free JamKazam app now and start playing with others!", + :title => "Download the Free JamKazam App" + }, + :client_dl_notrun => { + :subject => "Having trouble running the JamKazam application?", + :title => "Running the JamKazam App" + }, + :client_run_notgear => { + :subject => "Having trouble setting up your audio gear for JamKazam?", + :title => "Setting Up and Qualifying Your Audio Gear on JamKazam" + }, + :gear_notsess => { + :subject => "Having trouble getting into a session with other musicians?", + :title => "Tips on Getting into Sessions with Other Musicians" + }, + :sess_notgood => { + :subject => "Have you played in a “good” session on JamKazam yet?", + :title => "Having a Good Session on JamKazam" + }, + :reg_notinvite => { + :subject => "Invite your friends to JamKazam, best way to play online!", + :title => "Invite Your Friends to Come and Play with You" + }, + :reg_notconnect => { + :subject => "Make friends on JamKazam and play more music!", + :title => "Connecting with Friends on JamKazam" + }, + :reg_notlike => { + :subject => "Please give us a LIKE!", + :title => "Like/Follow JamKazam" + }, + :sess_notrecord => { + :subject => "Want to make recordings during your JamKazam sessions?", + :title => "Making & Sharing Recordings on JamKazam" + }, + } + + def self.subject(subtype=nil) + SUBTYPE_METADATA[subtype][:subject] if subtype + end + + def self.title(subtype=nil) + SUBTYPE_METADATA[subtype][:title] if subtype + end + + def self.subtype_trigger_interval(subtype) + [:reg_notlike, :sess_notrecord].include?(subtype) ? [7,14,21] : [2,5,14] + end + + def self.days_past_for_trigger_index(subtype, idx) + self.subtype_trigger_interval(subtype)[idx] + end + + def days_past_for_trigger_index(idx) + self.class.subtype_trigger_interval(self.sub_type)[idx] + end + + def fetch_recipients(since=nil) + since ||= Time.now - SINCE_WEEKS.weeks + User.geocoded_users + .email_opt_in + .where(['created_at < ?', since]) + .find_in_batches(batch_size: self::BATCH_SIZE) do |users| + new_musicians = users.inject({}) do |hh, uu| + if 0 < (nearby = uu.nearest_musicians).count + hh[uu] = nearby + end + hh + end + yield(new_musicians) if block_given? + end + end + + def trigger_date_constraint(trigger_idx, date_column) + intervals = self.class.subtype_trigger_interval(self.sub_type) + case trigger_idx + when 1 + return ["#{date_column} >= ? AND #{date_column} < ?", + Time.now - intervals[0].days, + Time.now - intervals[1].days] + else + return ["#{date_column} < ?", Time.now - intervals[trigger_idx].days] + end + end + + def fetch_client_notdl(trigger_idx=0) + fetched = [] + rel = User + .joins("LEFT JOIN email_batch_sets AS ebs ON ebs.user_id = users.id AND ebs.sub_type = '#{self.sub_type}'") + .email_opt_in + .musicians + .where("ebs.id IS NULL OR (SELECT MAX(trigger_index) FROM email_batch_sets ebs WHERE ebs.sub_type = '#{self.sub_type}' AND ebs.user_id = users.id ) < #{trigger_idx}") + .where("first_downloaded_client_at IS NULL") + .where(self.trigger_date_constraint(trigger_idx, "users.created_at")) + .find_in_batches(batch_size: 100) do |users| + block_given? ? yield(users) : fetched.concat(users) + end + fetched + end + + def deliver_batch_sets! + self.opt_in_count = 0 + sent = 0 + 3.times do |trigger_idx| + self.send("fetch_#{self.sub_type}", trigger_idx) do |users| + users.each do |uu| + self.email_batch_sets << (bset = EmailBatchSet.progress_set(self, uu, trigger_idx)) + ProgressMailer.send_reminder(bset).deliver + sent += 1 + end + end + end + self.sent_count = sent + self.save + self.did_batch_run! + end + + end +end diff --git a/ruby/lib/jam_ruby/models/email_batch_set.rb b/ruby/lib/jam_ruby/models/email_batch_set.rb index c3a324664..4b18e2654 100644 --- a/ruby/lib/jam_ruby/models/email_batch_set.rb +++ b/ruby/lib/jam_ruby/models/email_batch_set.rb @@ -3,6 +3,7 @@ module JamRuby self.table_name = "email_batch_sets" belongs_to :email_batch, :class_name => 'JamRuby::EmailBatch' + belongs_to :user, :class_name => 'JamRuby::User' def self.load_set(batch, user_ids) bset = self.new @@ -14,5 +15,31 @@ module JamRuby bset end + def self.progress_set(batch, user, trigger_idx) + bset = self.new + bset.email_batch = batch + bset.user = user + bset.started_at = Time.now + bset.batch_count = 1 + bset.trigger_index = trigger_idx + bset.sub_type = batch.sub_type + bset.save! + bset + end + + def subject + unless sub_type.blank? + return EmailBatchProgression.subject(self.sub_type) + end + '' + end + + def title + unless sub_type.blank? + return EmailBatchProgression.title(self.sub_type) + end + '' + end + end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index c9490e901..0d6dd9e16 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -446,7 +446,9 @@ FactoryGirl.define do end factory :email_batch_new_musician, :class => JamRuby::EmailBatchNewMusician do + end + factory :email_batch_progression, :class => JamRuby::EmailBatchProgression do end factory :notification, :class => JamRuby::Notification do diff --git a/ruby/spec/jam_ruby/models/email_batch_spec.rb b/ruby/spec/jam_ruby/models/email_batch_spec.rb index 8cbbfbc75..657bb49ca 100644 --- a/ruby/spec/jam_ruby/models/email_batch_spec.rb +++ b/ruby/spec/jam_ruby/models/email_batch_spec.rb @@ -1,23 +1,31 @@ require 'spec_helper' describe EmailBatch do - let (:email_batch) { FactoryGirl.create(:email_batch) } - let (:new_musician_batch) { FactoryGirl.create(:email_batch_new_musician) } - before(:each) do - BatchMailer.deliveries.clear - end + describe 'all users' do + let (:email_batch) { FactoryGirl.create(:email_batch) } - it 'has test emails setup' do - pending - expect(email_batch.test_emails.present?).to be true - expect(email_batch.pending?).to be true + before { pending } - users = email_batch.test_users - expect(email_batch.test_count).to eq(users.count) + before(:each) do + BatchMailer.deliveries.clear + end + + it 'has test emails setup' do + + expect(email_batch.test_emails.present?).to be true + expect(email_batch.pending?).to be true + + users = email_batch.test_users + expect(email_batch.test_count).to eq(users.count) + end end describe 'new musician' do + let (:new_musician_batch) { FactoryGirl.create(:email_batch_new_musician) } + + before { pending } + before(:each) do @u1 = FactoryGirl.create(:user, :lat => 37.791649, :lng => -122.394395, :email => 'jonathan@jamkazam.com', :subscribe_email => true, :created_at => Time.now - 3.weeks) @u2 = FactoryGirl.create(:user, :lat => 37.791649, :lng => -122.394395, :subscribe_email => true) @@ -26,7 +34,7 @@ describe EmailBatch do end it 'find new musicians with good score' do - EmailBatchNewMusician.fetch_recipients do |new_musicians| + new_musician_batch.fetch_recipients do |new_musicians| expect(new_musicians.count).to eq(2) num = (new_musicians.keys.map(&:id) - [@u1.id, @u4.id]).count expect(num).to eq(0) @@ -34,7 +42,6 @@ describe EmailBatch do end it 'has correct time since last batch' do - pending tt = EmailBatchNewMusician.time_since_last_batch expect(tt.to_i).to be < (Time.now - 1.week).to_i end @@ -49,4 +56,46 @@ describe EmailBatch do end + context 'user progress' do + + describe 'client not downloaded' do + let(:client_notdl) { FactoryGirl.create(:email_batch_progression, :sub_type => :client_notdl) } + + let(:user_client_notdl) { FactoryGirl.create(:user) } + let(:user_client_notdl_trigger0) { + FactoryGirl.create(:user, + :created_at => Time.now - client_notdl.days_past_for_trigger_index(0).days) + } + let(:user_client_notdl_trigger1) { + FactoryGirl.create(:user, + :created_at => Time.now - client_notdl.days_past_for_trigger_index(1).days) + } + let(:user_client_notdl_trigger2) { + FactoryGirl.create(:user, + :created_at => Time.now - client_notdl.days_past_for_trigger_index(2).days) + } + + it 'returns no users' do + pending + user_client_notdl + expect(client_notdl.fetch_client_notdl.count).to eq(0) + end + + it 'returns user first trigger' do + user_client_notdl_trigger0 + expect(client_notdl.fetch_client_notdl.count).to eq(1) + end + + it 'returns user second trigger' do + end + + it 'returns user third trigger' do + end + + it 'returns no users after third trigger' do + end + + end + end + end