* resolved merge

This commit is contained in:
Seth Call 2014-04-09 18:31:40 +01:00
commit cc7bbf3f6a
40 changed files with 716 additions and 187 deletions

View File

@ -70,6 +70,7 @@ gem 'sendgrid', '1.2.0'
gem 'geokit-rails'
gem 'postgres_ext', '1.0.0'
gem 'resque_mailer'
gem 'rest-client'
group :libv8 do
gem 'libv8', "~> 3.11.8"

View File

@ -1,50 +1,65 @@
ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
menu :label => 'Emails'
menu :label => 'Batch Emails', :parent => 'Email'
config.sort_order = 'updated_at DESC'
config.batch_actions = false
# config.clear_action_items!
config.clear_action_items!
config.filters = false
form :partial => 'form'
action_item :only => [:show] do
link_to('Edit Batch Email', edit_admin_batch_email_path(resource.id)) if resource.can_run_batch?
end
action_item :only => [:show] do
link_to("Test Batch (#{resource.test_count})",
batch_test_admin_batch_email_path(resource.id),
:confirm => "Run test batch with #{resource.test_count} emails?") if resource.can_run_test?
end
action_item :only => [:show] do
link_to("Deliver Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(resource.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?") if resource.can_run_batch?
end
action_item :only => [:show, :edit] do
link_to('Clone Batch Email', batch_clone_admin_batch_email_path(resource.id))
end
action_item do
link_to('New Batch Email', new_admin_batch_email_path)
end
index do
column 'Subject' do |pp| pp.subject end
column 'Updated' do |pp| pp.updated_at end
column 'From' do |pp| pp.from_email end
column 'Status' do |pp| pp.aasm_state end
column 'Test Emails' do |pp| pp.test_emails end
column 'Email Count' do |pp| pp.qualified_count end
column 'Send Count' do |pp| pp.sent_count end
column 'Started' do |pp| pp.started_at end
column 'Completed' do |pp| pp.completed_at end
column 'Send Test' do |pp|
link_to("Test Batch (#{pp.test_count})",
batch_test_admin_batch_email_path(pp.id),
:confirm => "Run test batch with #{pp.test_count} emails?")
column 'Subject' do |bb| bb.subject end
column 'Created' do |bb| bb.created_at end
column 'From' do |bb| bb.from_email end
column 'Status' do |bb| bb.aasm_state end
column 'Test Emails' do |bb| bb.test_emails end
column 'Email Count' do |bb| bb.opt_in_count end
column 'Sent Count' do |bb| bb.sent_count end
column 'Started' do |bb| bb.started_at end
column 'Completed' do |bb| bb.completed_at end
column 'Send Test' do |bb|
bb.can_run_test? ? link_to("Test Batch (#{bb.test_count})",
batch_test_admin_batch_email_path(bb.id),
:confirm => "Run test batch with #{bb.test_count} emails?") : ''
end
column 'Send Live' do |pp|
link_to("Live Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(pp.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
column 'Deliver Live' do |bb|
bb.can_run_batch? ? link_to("Deliver Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(bb.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?") : ''
end
column 'Clone' do |bb|
link_to("Clone", batch_clone_admin_batch_email_path(bb.id))
end
default_actions
end
action_item :only => :show do
link_to("Send Test Batch (#{resource.test_count})",
batch_test_admin_batch_email_path(resource.id),
:confirm => "Run test batch with #{resource.test_count} emails?")
end
action_item :only => :show do
link_to("Send Live Batch (#{User.email_opt_in.count})",
batch_send_admin_batch_email_path(resource.id),
:confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
end
show :title => 'Batch Email' do |obj|
panel 'Email Contents' do
attributes_table_for obj do
@ -59,9 +74,7 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
panel 'Sending Parameters' do
attributes_table_for obj do
row 'State' do |obj| obj.aasm_state end
row 'User Count' do |obj|
obj.qualified_count ? obj.qualified_count : User.email_opt_in.count
end
row 'Opt-in Count' do |obj| obj.opting_in_count end
row 'Sent Count' do |obj| obj.sent_count end
row 'Started' do |obj| obj.started_at end
row 'Completed' do |obj| obj.completed_at end
@ -70,7 +83,11 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
end
end
column do
panel 'Send Results' do
panel 'Send Chunks' do
table_for(sets = obj.email_batch_sets) do
column :started_at do |sets| sets.started_at.strftime('%b %d %Y, %H:%M') end
column :batch_count do |sets| sets.batch_count end
end
end
end
end
@ -81,7 +98,6 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
def create
batch = EmailBatch.create_with_params(params[:jam_ruby_email_batch])
redirect_to admin_batch_email_path(batch.id)
# redirect_to admin_batch_emails_path
end
def update
@ -92,15 +108,18 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
end
member_action :batch_test, :method => :get do
batch = EmailBatch.find(params[:id])
batch.send_test_batch
redirect_to admin_batch_email_path(batch.id)
resource.send_test_batch
redirect_to admin_batch_email_path(resource.id)
end
member_action :batch_send, :method => :get do
batch = EmailBatch.find(params[:id])
batch.deliver_batch
redirect_to admin_batch_email_path(batch.id)
resource.deliver_batch
redirect_to admin_batch_email_path(resource.id)
end
member_action :batch_clone, :method => :get do
bb = resource.clone
redirect_to edit_admin_batch_email_path(bb.id)
end
end

View File

@ -0,0 +1,29 @@
ActiveAdmin.register JamRuby::EmailError, :as => 'Email Errors' do
menu :label => 'Email Errors', :parent => 'Email'
config.batch_actions = false
config.filters = false
config.clear_action_items!
index do
column 'User' do |eerr|
eerr.user ? link_to(eerr.user.name, admin_user_path(eerr.user_id)) : 'N/A'
end
column 'Error Type' do |eerr| eerr.error_type end
column 'Email Address' do |eerr| eerr.email_address end
column 'Status' do |eerr| eerr.status end
column 'Reason' do |eerr| eerr.reason end
column 'Email Date' do |eerr| eerr.email_date end
end
controller do
def scoped_collection
@eerrors ||= end_of_association_chain
.includes([:user])
.order('email_date DESC')
end
end
end

View File

@ -25,7 +25,7 @@ ActiveAdmin.register JamRuby::MusicSessionHistory, :as => 'Music Session History
index :as => :block do |msh|
div :for => msh do
h3 "Session ##{msh.music_session_id}: #{msh.created_at.strftime('%b %d %Y, %H:%M')}"
h4 "(append URL with ?admin=0 to hide admin sessions)"
h4 "(append URL with ?admin=1 to show admin sessions)"
columns do
column do
panel 'Session Details' do

View File

@ -0,0 +1,7 @@
class AdminAuthorization < ActiveAdmin::AuthorizationAdapter
def authorized?(action, subject = nil)
subject.is_a?(EmailBatch) && :update == action ? subject.can_run_batch? : true
end
end

View File

@ -168,4 +168,8 @@ ActiveAdmin.setup do |config|
config.register_javascript 'autocomplete-rails.js'
config.register_stylesheet 'jquery.ui.theme.css'
config.authorization_adapter = "AdminAuthorization"
end

View File

@ -140,3 +140,5 @@ notifications_with_text.sql
notification_seen_at.sql
order_event_session.sql
emails.sql
email_batch.sql
user_progress_tracking2.sql

30
db/up/email_batch.sql Normal file
View File

@ -0,0 +1,30 @@
CREATE TABLE email_batch_sets (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
email_batch_id VARCHAR(64) REFERENCES email_batches(id) ON DELETE CASCADE,
started_at TIMESTAMP,
user_ids TEXT NOT NULL default '',
batch_count INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE email_batch_sets ADD CONSTRAINT email_batch_set_uniqkey UNIQUE (email_batch_id, started_at);
CREATE INDEX email_batch_set_fkidx ON email_batch_sets (email_batch_id);
CREATE TABLE email_errors (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
error_type VARCHAR(32),
email_address VARCHAR(256),
status VARCHAR(32),
email_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reason TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX email_error_user_fkidx ON email_errors(user_id);
CREATE INDEX email_error_address_idx ON email_errors(email_address);

View File

@ -6,9 +6,9 @@ CREATE TABLE email_batches (
aasm_state VARCHAR(32) NOT NULL default 'pending',
test_emails TEXT NOT NULL default '',
test_emails TEXT NOT NULL default 'test@jamkazam.com',
qualified_count INTEGER NOT NULL default 0,
opt_in_count INTEGER NOT NULL default 0,
sent_count INTEGER NOT NULL default 0,
lock_version INTEGER,
@ -20,19 +20,6 @@ CREATE TABLE email_batches (
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CREATE TABLE email_batch_results (
-- id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
-- email_batch_id VARCHAR(64) REFERENCES email_batches(id) ON DELETE CASCADE,
-- user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
-- error_type VARCHAR(32),
-- email_address VARCHAR(256),
-- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-- );
-- ALTER TABLE email_batch_results ADD CONSTRAINT email_batch_uniqkey UNIQUE (email_batch_id);
-- ALTER TABLE email_batch_results ADD CONSTRAINT email_user_uniqkey UNIQUE (user_id);
ALTER TABLE users ALTER COLUMN subscribe_email SET DEFAULT true;
UPDATE users SET subscribe_email = true WHERE subscribe_email = false;

View File

@ -0,0 +1,3 @@
-- tracks how users are progessing through the site: https://jamkazam.atlassian.net/wiki/pages/viewpage.action?pageId=3375145
ALTER TABLE users ADD COLUMN first_liked_us TIMESTAMP;

View File

@ -44,6 +44,7 @@ gem 'resque_mailer'
gem 'oj'
gem 'builder'
gem 'fog'
gem 'rest-client'
group :test do
gem 'simplecov', '~> 0.7.1'
@ -52,7 +53,6 @@ group :test do
gem "rspec", "2.11"
gem 'spork', '0.9.0'
gem 'database_cleaner', '0.7.0'
gem 'rest-client'
gem 'faker'
gem 'resque_spec'
end

View File

@ -17,6 +17,7 @@ require "postgres_ext"
require 'builder'
require 'cgi'
require 'resque_mailer'
require 'rest-client'
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"
@ -134,6 +135,8 @@ 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_set"
require "jam_ruby/models/email_error"
require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer"

View File

@ -11,6 +11,7 @@ module JamRuby
sendgrid_recipients(emails)
sendgrid_substitute(EmailBatch::VAR_FIRST_NAME, users.map(&:first_name))
sendgrid_substitute('@USERID', users.map(&:id))
batch.did_send(emails)
@ -24,12 +25,12 @@ module JamRuby
def send_batch_email(batch_id, user_ids)
users = User.find_all_by_id(user_ids)
batch = EmailBatch.where(:id => batch_id).limit(1).first
batch = EmailBatch.find(batch_id)
self._send_batch(batch, users)
end
def send_batch_email_test(batch_id)
batch = EmailBatch.where(:id => batch_id).limit(1).first
batch = EmailBatch.find(batch_id)
users = batch.test_users
self._send_batch(batch, users)
end

View File

@ -26,6 +26,9 @@ module JamRuby
sendgrid_category "Corporate"
sendgrid_unique_args :type => "feedback"
sendgrid_recipients([@email])
sendgrid_substitute('@USERID', [User.id_for_email(@email)])
mail(:to => "info@jamkazam.com", :subject => "Feedback received from #{@email} ") do |format|
format.text
format.html

View File

@ -23,6 +23,9 @@ module JamRuby
@signup_url = generate_signup_url(invited_user)
@suppress_user_has_account_footer = true
sendgrid_recipients([invited_user.email])
sendgrid_substitute('@USERID', [invited_user.id])
sendgrid_category "Welcome"
sendgrid_unique_args :type => "welcome_betauser"

View File

@ -25,6 +25,9 @@
sendgrid_category "Confirm Email"
sendgrid_unique_args :type => "confirm_email"
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format|
format.text
format.html
@ -36,6 +39,9 @@
sendgrid_category "Welcome"
sendgrid_unique_args :type => "welcome_message"
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
mail(:to => user.email, :subject => "Welcome to JamKazam") do |format|
format.text
format.html
@ -44,6 +50,10 @@
def password_changed(user)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "password_changed"
mail(:to => user.email, :subject => "JamKazam Password Changed") do |format|
format.text
@ -53,6 +63,10 @@
def password_reset(user, password_reset_url)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
@password_reset_url = password_reset_url
sendgrid_unique_args :type => "password_reset"
mail(:to => user.email, :subject => "JamKazam Password Reset") do |format|
@ -63,6 +77,10 @@
def updating_email(user)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "updating_email"
mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format|
format.text
@ -72,6 +90,10 @@
def updated_email(user)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "updated_email"
mail(:to => user.email, :subject => "JamKazam Email Changed") do |format|
format.text
@ -81,6 +103,10 @@
def new_musicians(user, new_nearby, host='www.jamkazam.com')
@user, @new_nearby, @host = user, new_nearby, host
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "new_musicians"
mail(:to => user.email, :subject => "JamKazam New Musicians in Your Area") do |format|
format.text
@ -97,6 +123,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
@ -110,6 +140,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
@ -123,6 +157,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
@ -136,6 +174,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:bcc => email, :subject => subject) do |format|
format.text
format.html
@ -149,6 +191,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
@ -162,6 +208,10 @@
@session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}"
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:bcc => email, :subject => subject) do |format|
format.text
format.html
@ -176,6 +226,10 @@
@session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}"
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:bcc => email, :subject => subject) do |format|
format.text
format.html
@ -189,6 +243,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:bcc => email, :subject => subject) do |format|
format.text
format.html
@ -202,6 +260,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:bcc => email, :subject => subject) do |format|
format.text
format.html
@ -215,6 +277,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
@ -228,6 +294,10 @@
@body = msg
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
@ -246,6 +316,10 @@
@sender_photo_url = sender_photo_url
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [User.id_for_email(email)])
mail(:to => email, :subject => subject) do |format|
format.text
format.html { render :layout => "from_user_mailer" }

View File

@ -47,6 +47,13 @@
<% end %>
</table>
<table align="center" width="650" cellpadding="10" bgcolor="#156572" cellspacing="0">
<tr>
<td align="center" valign="top">
<% [:twitter, :facebook, :google].each do |src| %>
<%= link_to(image_tag("http://www.jamkazam.com/assets/content/icon_#{src}.png", :style => "vertical-align:top"), "http://www.jamkazam.com/endorse/@USERID/#{src}") %>&nbsp;
<% end %>
</td>
</tr>
<tr>
<td align="center"><font size="1" color="#ffffff" face="Arial, Helvetica, sans-serif">Copyright &copy; <%= Time.now.year %> JamKazam, Inc. All rights reserved.</font>
</td>

View File

@ -0,0 +1,8 @@
Resque::Mailer.error_handler = lambda { |mailer, message, error, action, args|
# Necessary to re-enqueue jobs that receieve the SIGTERM signal
if error.is_a?(Resque::TermException)
Resque.enqueue(mailer, action, *args)
else
raise error
end
}

View File

@ -2,24 +2,27 @@ module JamRuby
class EmailBatch < ActiveRecord::Base
self.table_name = "email_batches"
has_many :email_batch_sets, :class_name => 'JamRuby::EmailBatchSet'
attr_accessible :from_email, :subject, :test_emails, :body
attr_accessible :lock_version, :qualified_count, :sent_count, :started_at, :completed_at
attr_accessible :lock_version, :opt_in_count, :sent_count, :started_at, :completed_at
default_scope :order => 'created_at DESC'
VAR_FIRST_NAME = '@FIRSTNAME'
VAR_LAST_NAME = '@LASTNAME'
DEFAULT_SENDER = "support@jamkazam.com"
BATCH_SIZE = 1000
BODY_TEMPLATE =<<FOO
Hello #{VAR_FIRST_NAME},
<p>Pellentesque facilisis metus ac cursus varius. Nunc laoreet diam mauris, et rhoncus quam commodo vel. Vestibulum nec diam lobortis, posuere sapien id, faucibus nulla. Vivamus vitae pellentesque massa. Proin quis nibh eu nibh imperdiet porttitor. </p>
<p>Paragraph 1 ... newline whitespace is significant for plain text conversions</p>
<p>Vestibulum mollis enim eu fringilla vulputate. Nam tincidunt, enim eget fringilla blandit, mi neque dictum dolor, non pellentesque libero erat sed massa. Morbi sodales lobortis eros, sed feugiat eros euismod eget. Nulla vulputate lobortis porttitor. </p>
<p>Paragraph 2 ... "#{VAR_FIRST_NAME}" will be replaced by users first name</p>
<p>Thanks for using JamKazam!</p>
The JamKazam Team
FOO
include AASM
@ -27,51 +30,58 @@ FOO
state :pending, :initial => true
state :testing
state :tested
state :batching
state :batched
state :delivering
state :delivered
state :disabled
event :enable do
transitions :from => :disabled, :to => :pending
end
event :reset do
transitions :from => [:disabled, :testing, :tested, :batching, :batched, :pending], :to => :pending
event :reset, :after => :did_reset do
transitions :from => [:disabled, :testing, :tested, :delivering, :delivered, :pending], :to => :pending
end
event :do_test_run, :before => :running_tests do
transitions :from => [:pending, :tested, :batched], :to => :testing
event :do_test_run, :before => :will_run_tests do
transitions :from => [:pending, :tested, :delivered], :to => :testing
end
event :did_test_run, :after => :ran_tests do
transitions :from => :testing, :to => :tested
end
event :do_batch_run, :before => :running_batch do
transitions :from => [:tested, :pending, :batched], :to => :batching
event :do_batch_run, :before => :will_run_batch do
transitions :from => [:tested, :pending, :delivered], :to => :delivering
end
event :did_batch_run, :after => :ran_batch do
transitions :from => :batching, :to => :batched
transitions :from => :delivering, :to => :delivered
end
event :disable do
transitions :from => [:pending, :tested, :batched], :to => :disabled
transitions :from => [:pending, :tested, :delivered], :to => :disabled
end
end
# has_many :email_batch_results, :class_name => 'JamRuby::EmailBatchResult'
def self.new(*args)
oo = super
oo.body = BODY_TEMPLATE
oo
end
def self.create_with_params(params)
obj = self.new
params.each { |kk,vv| vv.strip! }
params[:body] = BODY_TEMPLATE if params[:body].empty?
obj.update_with_conflict_validation(params)
obj
end
def can_run_batch?
self.tested? || self.pending?
end
def can_run_test?
self.test_emails.present? && (self.tested? || self.pending?)
end
def deliver_batch
self.perform_event('do_batch_run!')
User.email_opt_in.find_in_batches(batch_size: 1000) do |users|
if 'test' == Rails.env
BatchMailer.send_batch_email(self.id, users.map(&:id)).deliver!
else
BatchMailer.send_batch_email(self.id, users.map(&:id)).deliver
end
User.email_opt_in.find_in_batches(batch_size: BATCH_SIZE) do |users|
self.email_batch_sets << EmailBatchSet.deliver_set(self.id, users.map(&:id))
end
end
@ -92,7 +102,7 @@ FOO
def send_test_batch
self.perform_event('do_test_run!')
if 'test' == Rails.env
if 'test'==Rails.env
BatchMailer.send_batch_email_test(self.id).deliver!
else
BatchMailer.send_batch_email_test(self.id).deliver
@ -106,8 +116,8 @@ FOO
def did_send(emails)
self.update_with_conflict_validation({ :sent_count => self.sent_count + emails.size })
if self.sent_count >= self.qualified_count
if batching?
if self.sent_count >= self.opt_in_count
if delivering?
self.perform_event('did_batch_run!')
elsif testing?
self.perform_event('did_test_run!')
@ -128,26 +138,28 @@ FOO
def update_with_conflict_validation(*args)
num_try = 0
update_attributes(*args)
rescue ActiveRecord::StaleObjectError
num_try += 1
if 5 > num_try
self.reload
retry
begin
update_attributes(*args)
rescue ActiveRecord::StaleObjectError
num_try += 1
if 5 > num_try
self.reload
sleep(0.25)
retry
end
end
end
def running_batch
self.update_with_conflict_validation({:qualified_count => User.email_opt_in.count,
def will_run_batch
self.update_with_conflict_validation({:opt_in_count => User.email_opt_in.count,
:sent_count => 0,
:started_at => Time.now
})
end
def running_tests
self.update_with_conflict_validation({:qualified_count => self.test_count,
:sent_count => 0,
:started_at => Time.now
def will_run_tests
self.update_with_conflict_validation({:opt_in_count => self.test_count,
:sent_count => 0
})
end
@ -159,5 +171,29 @@ FOO
self.update_with_conflict_validation({ :completed_at => Time.now })
end
def clone
bb = EmailBatch.new
bb.subject = self.subject
bb.body = self.body
bb.from_email = self.from_email
bb.test_emails = self.test_emails
bb.save!
bb
end
def opting_in_count
0 < opt_in_count ? opt_in_count : User.email_opt_in.count
end
def did_reset
self.email_batch_sets.map(&:destroy)
self.update_with_conflict_validation({
:opt_in_count => 0,
:sent_count => 0,
:started_at => nil,
:completed_at => nil,
})
end
end
end

View File

@ -0,0 +1,23 @@
module JamRuby
class EmailBatchSet < ActiveRecord::Base
self.table_name = "email_batch_sets"
belongs_to :email_batch, :class_name => 'JamRuby::EmailBatch'
def self.deliver_set(batch_id, user_ids)
bset = self.new
bset.email_batch_id = batch_id
bset.user_ids = user_ids.join(',')
bset.started_at = Time.now
bset.batch_count = user_ids.size
bset.save!
if 'test' == Rails.env
BatchMailer.send_batch_email(bset.email_batch_id, user_ids).deliver!
else
BatchMailer.send_batch_email(bset.email_batch_id, user_ids).deliver
end
bset
end
end
end

View File

@ -0,0 +1,80 @@
module JamRuby
class EmailError < ActiveRecord::Base
self.table_name = "email_errors"
belongs_to :user, :class_name => 'JamRuby::User'
default_scope :order => 'email_date DESC'
ERR_BOUNCE = :bounce
ERR_INVALID = :invalid
SENDGRID_UNAME = 'jamkazam'
SENDGRID_PASSWD = 'jamjamblueberryjam'
def self.sendgrid_url(resource, action='get', params='')
start_date, end_date = self.date_range
"https://api.sendgrid.com/api/#{resource}.#{action}.json?api_user=#{EmailError::SENDGRID_UNAME}&api_key=#{EmailError::SENDGRID_PASSWD}&date=1&start_date=#{start_date.strftime('%Y-%m-%d')}&end_date=#{end_date.strftime('%Y-%m-%d')}&#{params}"
end
def self.date_range
tt = Time.now
if eerr = self.first
return [eerr.email_date, tt]
end
[tt - 1.year, tt]
end
def self.did_capture?(email_addy)
self.where(:email_address => email_addy).limit(1).first.present?
end
def self.bounce_errors
uu = self.sendgrid_url('bounces')
response = RestClient.get(uu)
if 200 == response.code
return JSON.parse(response.body).collect do |jj|
next if self.did_capture?(jj['email'])
ee = EmailError.new
ee.error_type = 'bounces'
ee.email_address = jj['email']
ee.user_id = User.where(:email => ee.email_address).pluck(:id).first
ee.status = jj['status']
ee.email_date = jj['created']
ee.reason = jj['reason']
ee.save!
# RestClient.delete(self.sendgrid_url('bounces', 'delete', "email=#{ee.email_address}"))
ee
end
end
end
def self.invalid_errors
uu = self.sendgrid_url('invalidemails')
response = RestClient.get(uu)
if 200 == response.code
return JSON.parse(response.body).collect do |jj|
next if self.did_capture?(jj['email'])
ee = EmailError.new
ee.error_type = 'invalidemails'
ee.email_address = jj['email']
ee.user_id = User.where(:email => ee.email_address).pluck(:id).first
ee.email_date = jj['created']
ee.reason = jj['reason']
ee.save!
uu =
# RestClient.delete(self.sendgrid_url('invalidemails', 'delete', "email=#{ee.email_address}"))
ee
end
end
end
def self.capture_errors
EmailError.bounce_errors
EmailError.invalid_errors
end
end
end

View File

@ -1174,6 +1174,10 @@ module JamRuby
# self.password_digest = encrypted_password
#end
def self.id_for_email(email)
User.where(:email => email).limit(1).pluck(:id).first
end
# end devise compatibility
private
def create_remember_token

View File

@ -0,0 +1,15 @@
module JamRuby
class EmailErrorCollector
extend Resque::Plugins::LonelyJob
@queue = :email_error_collector
@@log = Logging.logger[EmailErrorCollector]
def self.perform
@@log.debug("waking up")
EmailError.capture_errors
@@log.debug("done")
end
end
end

View File

@ -75,6 +75,7 @@ gem 'resque_mailer'
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
gem 'rest_client'
group :development, :test do
gem 'rspec-rails'
@ -131,4 +132,3 @@ group :package do
gem 'fpm'
end

View File

@ -6,7 +6,6 @@
var logger = context.JK.logger;
var rest = context.JK.Rest();
var instrumentLogoMap = context.JK.getInstrumentIconMap24();
var hoverSelector = "#band-hover";
this.showBubble = function() {
@ -28,7 +27,7 @@
instrumentHtml = '<td><div class="nowrap">';
if (val.instruments) { // @FIXME: edge case for Test user that has no instruments?
$.each(val.instruments, function(index, instrument) {
instrumentHtml += '<img src="' + instrumentLogoMap[instrument.instrument_id] + '" width="24" height="24" />&nbsp;';
instrumentHtml += '<img src="' + context.JK.getInstrumentIcon24(instrument.instrument_id) + '" width="24" height="24" />&nbsp;';
});
}

View File

@ -6,7 +6,6 @@
var logger = context.JK.logger;
var rest = context.JK.Rest();
var instrumentLogoMap = context.JK.getInstrumentIconMap24();
var hoverSelector = "#musician-hover";
this.showBubble = function() {
@ -20,7 +19,7 @@
// instruments
var instrumentHtml = '';
$.each(response.instruments, function(index, val) {
instrumentHtml += '<div class="left mr10 mb"><img src="' + instrumentLogoMap[val.instrument_id] + '" width="24" height="24" /></div>';
instrumentHtml += '<div class="left mr10 mb"><img src="' + context.JK.getInstrumentIcon24(val.instrument_id) + '" width="24" height="24" /></div>';
});
// followings

View File

@ -5,7 +5,6 @@
context.JK.RecordingHoverBubble = function(recordingId, position) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var instrumentLogoMap = context.JK.getInstrumentIconMap24();
var hoverSelector = "#recording-hover";
function deDupTracks(recordedTracks) {
@ -61,7 +60,7 @@
instrumentHtml = '<td><div class="nowrap">';
$.each(val.instrument_ids, function(index, val) {
instrumentHtml += '<img src="' + instrumentLogoMap[val] + '" width="24" height="24" />&nbsp;&nbsp;';
instrumentHtml += '<img src="' + context.JK.getInstrumentIcon24(val) + '" width="24" height="24" />&nbsp;&nbsp;';
})
instrumentHtml += '</div></td>';

View File

@ -6,7 +6,6 @@
var logger = context.JK.logger;
var rest = context.JK.Rest();
var instrumentLogoMap = context.JK.getInstrumentIconMap24();
var hoverSelector = "#session-hover";
this.showBubble = function() {
@ -28,7 +27,7 @@
instrumentHtml = '<td><div class="nowrap">';
var instruments = val.instruments.split("|");
$.each(instruments, function(index, instrument) {
instrumentHtml += '<img src="' + instrumentLogoMap[instrument] + '" width="24" height="24" />&nbsp;';
instrumentHtml += '<img src="' + context.JK.getInstrumentIcon24(instrument) + '" width="24" height="24" />&nbsp;';
});
instrumentHtml += '</div></td>';

View File

@ -28,7 +28,6 @@
}
function togglePlay() {
if(playing) {
stopPlay();
}
@ -51,10 +50,10 @@
function like() {
rest.addRecordingLike(recordingId, claimedRecordingId, JK.currentUserId)
.done(function(response) {
$("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1);
$("#btnLike", $scope).unbind("click");
});
.done(function(response) {
$("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1);
$("#btnLike", $scope).unbind("click");
});
}
function play() {
@ -68,21 +67,35 @@
var comment = $("#txtRecordingComment").val();
if ($.trim(comment).length > 0) {
rest.addRecordingComment(recordingId, JK.currentUserId, comment)
.done(function(response) {
$("#spnCommentCount", $scope).html(parseInt($("#spnCommentCount").text()) + 1);
var template = $('#template-landing-comment').html();
var commentHtml = context.JK.fillTemplate(template, {
avatar_url: context.JK.currentUserAvatarUrl,
name: context.JK.currentUserName,
comment: comment
});
$(".landing-comment-scroller").prepend(commentHtml);
});
.done(function(response) {
$("#spnCommentCount", $scope).html(parseInt($("#spnCommentCount").text()) + 1);
renderComment(comment, context.JK.currentUserId, context.JK.currentUserName,
context.JK.currentUserAvatarUrl, $.timeago(Date.now()), context.JK.currentUserMusician, false);
});
}
}
function renderComment(comment, userId, userName, userAvatarUrl, timeago, musician, append) {
var template = $('#template-landing-comment').html();
var commentHtml = context.JK.fillTemplate(template, {
avatar_url: userAvatarUrl,
user_id: userId,
hoverAction: musician ? "musician" : "fan",
name: userName,
comment: comment,
timeago: timeago
});
if (append) {
$(".landing-comment-scroller").append(commentHtml);
}
else {
$(".landing-comment-scroller").prepend(commentHtml);
}
context.JK.bindHoverEvents();
}
function initialize(_claimedRecordingId, _recordingId) {
recordingId = _recordingId;
claimedRecordingId = _claimedRecordingId;
@ -90,8 +103,7 @@
$('.timeago').timeago();
$playButton.click(togglePlay);
$controls.bind('statechange.listenRecording', stateChange);
$controls.listenRecording({recordingId: recordingId, claimedRecordingId: claimedRecordingId, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'});
$controls.listenRecording({recordingId: recordingId, claimedRecordingId: claimedRecordingId, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'});
if (JK.currentUserId) {
var shareDialog = new JK.ShareDialog(JK.app, claimedRecordingId, "recording");
@ -116,6 +128,32 @@
$("#btnLike").click(like);
$("#btnPlay").click(play);
$playButton.trigger('click');
pollForUpdates(claimedRecordingId);
}
function pollForUpdates(claimedRecordingId) {
$(".landing-comment-scroller").empty();
rest.getClaimedRecording(claimedRecordingId)
.done(function(response) {
if (response.recording && response.recording.comments) {
$("#spnPlayCount", $scope).html(response.recording.play_count);
$("#spnCommentCount", $scope).html(response.recording.comment_count);
$("#spnLikeCount", $scope).html(response.recording.like_count);
$.each(response.recording.comments, function(index, val) {
renderComment(val.comment, val.creator.id, val.creator.name,
context.JK.resolveAvatarUrl(val.creator.photo_url), $.timeago(val.created_at), val.creator.musician, true);
});
setTimeout(function() {
pollForUpdates(claimedRecordingId);
}, 60000);
}
})
.fail(function(xhr) {
});
}
this.initialize = initialize;

View File

@ -4,37 +4,53 @@
var logger = context.JK.logger;
var rest = new JK.Rest();
var sessionId = null;
var $scope = $(".landing-details");
var $controls = null;
var $status = null;
var $playButton = $('.play-button');
var playing = false;
function like() {
rest.addSessionLike(sessionId, JK.currentUserId)
.done(function(response) {
$("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1);
$("#btnLike").unbind("click");
});
.done(function(response) {
$("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1);
$("#btnLike").unbind("click");
});
}
function addComment() {
var comment = $("#txtSessionComment").val();
if ($.trim(comment).length > 0) {
rest.addSessionComment(sessionId, JK.currentUserId, comment)
.done(function(response) {
$("#spnCommentCount").html(parseInt($("#spnCommentCount").text()) + 1);
var template = $('#template-landing-comment').html();
var commentHtml = context.JK.fillTemplate(template, {
avatar_url: context.JK.currentUserAvatarUrl,
name: context.JK.currentUserName,
comment: comment
});
$(".landing-comment-scroller").prepend(commentHtml);
});
rest.addSessionComment(sessionId, JK.currentUserId, comment)
.done(function(response) {
$("#spnCommentCount").html(parseInt($("#spnCommentCount").text()) + 1);
renderComment(comment, context.JK.currentUserId, context.JK.currentUserName,
context.JK.currentUserAvatarUrl, $.timeago(Date.now()), context.JK.currentUserMusician, false);
});
}
}
function renderComment(comment, userId, userName, userAvatarUrl, timeago, musician, append) {
var template = $('#template-landing-comment').html();
var commentHtml = context.JK.fillTemplate(template, {
avatar_url: userAvatarUrl,
user_id: userId,
hoverAction: musician ? "musician" : "fan",
name: userName,
comment: comment,
timeago: timeago
});
if (append) {
$(".landing-comment-scroller").append(commentHtml);
}
else {
$(".landing-comment-scroller").prepend(commentHtml);
}
context.JK.bindHoverEvents();
}
function stateChange(e, data) {
if(data.displayText)
{
@ -87,34 +103,57 @@
$controls.bind('statechange.listenBroadcast', stateChange);
context.JK.prettyPrintElements($('time.duration').show());
context.JK.TickDuration(null);
$('.play-button').click(togglePlay);
$playButton.click(togglePlay);
sessionId = musicSessionId;
if (JK.currentUserId) {
var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session");
shareDialog.initialize(JK.FacebookHelperInstance);
var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session");
shareDialog.initialize(JK.FacebookHelperInstance);
$("#btnShare").click(function(e) {
shareDialog.showDialog();
});
$("#btnShare").click(function(e) {
shareDialog.showDialog();
});
$("#btnPostComment").click(function(e) {
if ($.trim($("#txtSessionComment").val()).length > 0) {
addComment();
$("#txtSessionComment").val('');
$("#txtSessionComment").blur();
}
});
$("#btnPostComment").click(function(e) {
if ($.trim($("#txtSessionComment").val()).length > 0) {
addComment();
$("#txtSessionComment").val('');
$("#txtSessionComment").blur();
}
});
}
else {
$("#txtSessionComment").attr("disabled", "disabled");
$("#txtSessionComment").val("You must be logged in to add a comment.");
$("#txtSessionComment").attr("disabled", "disabled");
$("#txtSessionComment").val("You must be logged in to add a comment.");
}
$("#btnLike").click(like);
$playButton.trigger('click');
pollForUpdates(musicSessionId);
}
function pollForUpdates(musicSessionId) {
$(".landing-comment-scroller").empty();
rest.getSessionHistory(musicSessionId)
.done(function(response) {
if (response && response.comments) {
$("#spnCommentCount", $scope).html(response.comment_count);
$("#spnLikeCount", $scope).html(response.like_count);
$.each(response.comments, function(index, val) {
renderComment(val.comment, val.creator.id, val.creator.name,
context.JK.resolveAvatarUrl(val.creator.photo_url), $.timeago(val.created_at), val.creator.musician, true);
});
setTimeout(function() {
pollForUpdates(musicSessionId);
}, 60000);
}
})
.fail(function(xhr) {
});
}
this.initialize = initialize;

View File

@ -368,6 +368,23 @@ class UsersController < ApplicationController
render :layout => "landing"
end
def endorse
if uu = User.where(['id = ? AND first_liked_us IS NULL',params[:id]]).limit(1).first
uu.first_liked_us = Time.now
uu.save!
end if params[:id].present?
url, service = 'http://www.jamkazam.com', params[:service]
if 'twitter'==service
url = 'https://twitter.com/jamkazam'
elsif 'facebook'==service
url = 'https://www.facebook.com/JamKazam'
elsif 'google'==service
url = 'https://plus.google.com/u/0/106619885929396862606/about'
end
redirect_to url
end
private
def is_native_client

View File

@ -35,7 +35,7 @@ child(:recording => :recording) {
attributes :comment, :created_at
child(:user => :creator) {
attributes :id, :first_name, :last_name, :photo_url
attributes :id, :first_name, :last_name, :name, :photo_url, :musician
}
}
}

View File

@ -22,4 +22,12 @@ child(:music_session_user_histories => :users) {
child(:user => :user) {
attributes :name, :photo_url
}
}
child(:comments => :comments) {
attributes :comment, :created_at
child(:user => :creator) {
attributes :id, :first_name, :last_name, :name, :photo_url, :musician
}
}

View File

@ -92,10 +92,12 @@
JK.currentUserId = '<%= current_user.id %>';
JK.currentUserAvatarUrl = JK.resolveAvatarUrl('<%= current_user.photo_url %>');
JK.currentUserName = '<%= current_user.name %>';
JK.currentUserMusician = '<%= current_user.musician %>';
<% else %>
JK.currentUserId = null;
JK.currentUserAvatarUrl = null;
JK.currentUserName = null;
JK.currentUserMusician = null;
<% end %>
JK.app = JK.JamKazam();

View File

@ -7,38 +7,23 @@
<textarea id="<%= id %>" class="w100 p5 f15" rows="2" placeholder="Enter a comment..."></textarea>
</div>
<div class="right mr20">
<a href="#" id="btnPostComment" class="button-orange">POST</a>
<a id="btnPostComment" class="button-orange">POST</a>
</div>
<br clear="all" />
<div class="landing-comment-scroller">
<% comments.each do |c| %>
<% hoverAction = c.user.musician ? "musician" : "fan" %>
<div user-id="<%= c.user.id %>" hoveraction="<%= hoverAction %>" class="avatar-small mr10">
<% unless c.user.photo_url.blank? %>
<%= image_tag "#{c.user.photo_url}", {:alt => ""} %>
<% else %>
<%= image_tag "shared/avatar_generic.png", {:alt => ""} %>
<% end %>
</div>
<div class="w80 left p10 lightgrey mt10">
<a user-id="<%= c.user.id %>" hoveraction="<%= hoverAction %>" href="#"><%= c.user.name %></a>&nbsp;<%= c.comment %>
<br />
<div class="f12 grey mt5"><%= c.created_at.strftime("%b %e %Y, %l:%M %p") %></div>
</div>
<br clear="all" />
<% end %>
</div>
</div>
<script type="text/template" id="template-landing-comment">
<div class="avatar-small mr10">
<img src="{avatar_url}" alt="" />
<div user-id="{user_id}" hoveraction="{hoverAction}" class="avatar-small mr10">
<img src="{avatar_url}" alt="" />
</div>
<div class="w80 left p10 lightgrey mt10">
<a href="#">{name}</a>&nbsp;{comment}
<br />
<div class="f12 grey mt5">Just now</div>
</div>
<br clear="all" />
<a user-id="{user_id}" hoveraction="{hoverAction}">{name}</a>&nbsp;{comment}
<br />
<div class="f12 grey mt5">{timeago}</div>
</div>
<br clear="all" />
</script>

View File

@ -63,6 +63,8 @@ SampleApp::Application.routes.draw do
match '/events/:slug', to: 'events#show', :via => :get, :as => 'event'
match '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse'
# temporarily allow for debugging--only allows admini n
match '/listen_in', to: 'spikes#listen_in'

View File

@ -19,3 +19,8 @@ CleanupFacebookSignup:
class: "JamRuby::CleanupFacebookSignup"
description: "Deletes facebook_signups that are old"
EmailErrorCollector:
cron: "0 14 * * *"
class: "JamRuby::EmailErrorCollector"
description: "Collects sendgrid email errors"

View File

@ -0,0 +1,50 @@
require 'spec_helper'
describe "Landing", :js => true, :type => :feature, :capybara_feature => true do
let (:user) { FactoryGirl.create(:user) }
before(:all) do
MusicSessionHistory.delete_all
ClaimedRecording.delete_all
Recording.delete_all
end
before(:each) do
MusicSessionHistory.delete_all
end
let (:claimed_recording) { FactoryGirl.create(:claimed_recording) }
it "should render comments" do
pending "weird error"
recording = ClaimedRecording.first
comment = "test comment"
timestamp = "less than a minute ago"
url = "/recordings/#{claimed_recording.id}"
visit url
fill_in "txtRecordingComment", with: comment
find('#btnPostComment').trigger(:click)
# (1) Test a user creating a comment and ensure it displays.
# comment body
find('div', text: comment)
# timestamp
find('div', text: timestamp)
# (2) Test a user visiting a landing page with an existing comment.
# re-visit page to reload from database
visit url
# comment body
find('div', text: comment)
# timestamp
find('div', text: timestamp)
end
end

View File

@ -0,0 +1,47 @@
require 'spec_helper'
describe "Landing", :js => true, :type => :feature, :capybara_feature => true do
let (:user) { FactoryGirl.create(:user) }
before(:all) do
MusicSessionHistory.delete_all
end
before(:each) do
create_session(creator: user)
formal_leave_by(user)
end
it "should render comments" do
pending "weird error"
msh = MusicSessionHistory.first
comment = "test comment"
timestamp = "less than a minute ago"
url = "/sessions/#{msh.id}"
visit url
# (1) Test a user creating a comment and ensure it displays.
fill_in "txtSessionComment", with: comment
find('#btnPostComment').trigger(:click)
# comment body
find('div', text: comment)
# timestamp
find('div', text: timestamp)
# (2) Test a user visiting a landing page with an existing comment.
# re-visit page to reload from database
visit url
# comment body
find('div', text: comment)
# timestamp
find('div', text: timestamp)
end
end

View File

@ -46,6 +46,7 @@ gem 'resque_mailer'
gem 'geokit'
gem 'geokit-rails', '2.0.1'
gem 'mime-types', '1.25.1'
gem 'rest-client'
group :development do
gem 'pry'