diff --git a/admin/Gemfile b/admin/Gemfile index c8b1d1772..8a47660de 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -116,3 +116,7 @@ group :test do gem 'simplecov-rcov' end +gem 'pry' +gem 'pry-remote' +gem 'pry-stack_explorer' +gem 'pry-debugger' diff --git a/admin/app/admin/affiliate_users.rb b/admin/app/admin/affiliate_users.rb new file mode 100644 index 000000000..b21d512eb --- /dev/null +++ b/admin/app/admin/affiliate_users.rb @@ -0,0 +1,31 @@ +ActiveAdmin.register JamRuby::User, :as => 'Referrals' do + + menu :label => 'Referrals', :parent => 'Affiliates' + + config.batch_actions = false + config.clear_action_items! + config.filters = false + + index do + column 'User' do |oo| link_to(oo.name, "http://www.jamkazam.com/client#/profile/#{oo.id}", {:title => oo.name}) end + column 'Email' do |oo| oo.email end + column 'Created' do |oo| oo.created_at end + column 'Partner' do |oo| oo.affiliate_referral.partner_name end + end + + controller do + + def scoped_collection + rel = end_of_association_chain + .includes([:affiliate_referral]) + .order('created_at DESC') + if (ref_id = params[AffiliatePartner::PARAM_REFERRAL]).present? + qq = ['affiliate_referral_id = ?', ref_id] + else + qq = ['affiliate_referral_id IS NOT NULL'] + end + @users ||= rel.where(qq) + end + + end +end diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb new file mode 100644 index 000000000..e517b3e12 --- /dev/null +++ b/admin/app/admin/affiliates.rb @@ -0,0 +1,49 @@ +ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do + + menu :label => 'Partners', :parent => 'Affiliates' + + config.sort_order = 'created_at DESC' + config.batch_actions = false + # config.clear_action_items! + config.filters = false + + form :partial => 'form' + + index do + column 'User' do |oo| link_to(oo.partner_user.name, "http://www.jamkazam.com/client#/profile/#{oo.partner_user.id}", {:title => oo.partner_user.name}) end + column 'Email' do |oo| oo.partner_user.email end + column 'Name' do |oo| oo.partner_name end + column 'Code' do |oo| oo.partner_code end + column 'Referral Count' do |oo| oo.referral_user_count end + # column 'Referrals' do |oo| link_to('View', admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => oo.id)) end + default_actions + end + + controller do + + def show + redirect_to admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => resource.id) + end + + def create + obj = AffiliatePartner.create_with_params(params[:jam_ruby_affiliate_partner]) + if obj.errors.present? + set_resource_ivar(obj) + render active_admin_template('new') + else + redirect_to admin_affiliates_path + end + end + + def update + obj = resource + vals = params[:jam_ruby_affiliate_partner] + obj.partner_name = vals[:partner_name] + obj.user_email = vals[:user_email] if vals[:user_email].present? + obj.save! + set_resource_ivar(obj) + render active_admin_template('show') + end + + end +end diff --git a/admin/app/admin/email_batch.rb b/admin/app/admin/email_batch.rb index d074fd0d4..e5743eeb8 100644 --- a/admin/app/admin/email_batch.rb +++ b/admin/app/admin/email_batch.rb @@ -97,12 +97,14 @@ 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) + set_resource_ivar(batch) + render active_admin_template('show') end def update resource.update_with_conflict_validation(params[:jam_ruby_email_batch]) - redirect_to admin_batch_email_path(resource.id) + set_resource_ivar(resource) + render active_admin_template('show') end end diff --git a/admin/app/models/admin_authorization.rb b/admin/app/models/admin_authorization.rb index 692d118f1..809991226 100644 --- a/admin/app/models/admin_authorization.rb +++ b/admin/app/models/admin_authorization.rb @@ -1,7 +1,13 @@ class AdminAuthorization < ActiveAdmin::AuthorizationAdapter def authorized?(action, subject = nil) - subject.is_a?(EmailBatch) && :update == action ? subject.can_run_batch? : true + if subject.is_a?(EmailBatch) && :update == action + subject.can_run_batch? + elsif subject.is_a?(AffiliatePartner) && :destroy == action + false + else + true + end end end diff --git a/admin/app/views/admin/affiliates/_form.html.erb b/admin/app/views/admin/affiliates/_form.html.erb new file mode 100644 index 000000000..a4f14416e --- /dev/null +++ b/admin/app/views/admin/affiliates/_form.html.erb @@ -0,0 +1,13 @@ +<%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_affiliates_path : "/admin/affiliates/#{resource.id}") do |f| %> + <%= f.semantic_errors *f.object.errors.keys %> + <%= f.inputs do %> + <%= f.input(:user_email, :input_html => {:maxlength => 255}) %> + <%= f.input(:partner_name, :input_html => {:maxlength => 128}) %> + <% if resource.new_record? %> + <%= f.input(:partner_code, :input_html => {:maxlength => 128}) %> + <% else %> + <%= f.input(:partner_code, :input_html => {:maxlength => 128, :readonly => 'readonly'}) %> + <% end %> + <% end %> + <%= f.actions %> +<% end %> diff --git a/admin/config/environments/test.rb b/admin/config/environments/test.rb index 6a51a23eb..1f9229001 100644 --- a/admin/config/environments/test.rb +++ b/admin/config/environments/test.rb @@ -40,4 +40,6 @@ JamAdmin::Application.configure do config.twitter_app_id = 'e7hGc71gmcBgo6Wvdta6Sg' config.twitter_app_secret = 'PfG1jAUMnyrimPcDooUVQaJrG1IuDjUyGg5KciOo' + + config.redis_host = "localhost:6379:1" # go to another db to not cross pollute into dev/production redis dbs end diff --git a/db/manifest b/db/manifest index 843c69235..4d691b123 100755 --- a/db/manifest +++ b/db/manifest @@ -143,4 +143,6 @@ emails.sql email_batch.sql user_progress_tracking2.sql bands_did_session.sql -email_change_default_sender.sql \ No newline at end of file +email_change_default_sender.sql +affiliate_partners.sql +chat_messages.sql \ No newline at end of file diff --git a/db/up/affiliate_partners.sql b/db/up/affiliate_partners.sql new file mode 100644 index 000000000..67f7dfd97 --- /dev/null +++ b/db/up/affiliate_partners.sql @@ -0,0 +1,15 @@ +CREATE TABLE affiliate_partners ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + partner_name VARCHAR(128) NOT NULL, + partner_code VARCHAR(128) NOT NULL, + partner_user_id VARCHAR(64) NOT NULL, + user_email VARCHAR(255), + referral_user_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX affiliate_partners_code_idx ON affiliate_partners(partner_code); +CREATE INDEX affiliate_partners_user_idx ON affiliate_partners(partner_user_id); + +ALTER TABLE users ADD COLUMN affiliate_referral_id VARCHAR(64) REFERENCES affiliate_partners(id); diff --git a/db/up/chat_messages.sql b/db/up/chat_messages.sql new file mode 100644 index 000000000..01a0fcd14 --- /dev/null +++ b/db/up/chat_messages.sql @@ -0,0 +1,8 @@ +CREATE TABLE chat_messages +( + id character varying(64) NOT NULL DEFAULT uuid_generate_v4(), + user_id character varying(64), + music_session_id character varying(64), + messsage TEXT NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 1df3bebf8..51ec3308d 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -52,6 +52,7 @@ message ClientMessage { // text message TEXT_MESSAGE = 236; + CHAT_MESSAGE = 237; MUSICIAN_SESSION_FRESH = 240; MUSICIAN_SESSION_STALE = 245; @@ -130,6 +131,7 @@ message ClientMessage { // text message optional TextMessage text_message = 236; + optional ChatMessage chat_message = 237; optional MusicianSessionFresh musician_session_fresh = 240; optional MusicianSessionStale musician_session_stale = 245; @@ -397,6 +399,14 @@ message TextMessage { optional bool clipped_msg = 7; } +message ChatMessage { + optional string sender_name = 1; + optional string sender_id = 2; + optional string msg = 3; + optional string msg_id = 4; + optional string created_at = 5; +} + // route_to: client: // sent by server to let the rest of the participants know a client has become active again after going stale message MusicianSessionFresh { diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index ef426e9a8..f596d402d 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -140,6 +140,7 @@ 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" +require "jam_ruby/models/affiliate_partner" include Jampb diff --git a/ruby/lib/jam_ruby/lib/em_helper.rb b/ruby/lib/jam_ruby/lib/em_helper.rb index 9e826b74c..3e290c47f 100644 --- a/ruby/lib/jam_ruby/lib/em_helper.rb +++ b/ruby/lib/jam_ruby/lib/em_helper.rb @@ -36,7 +36,7 @@ module JamWebEventMachine end - def self.run_em(calling_thread) + def self.run_em(calling_thread = nil) EM.run do # this is global because we need to check elsewhere if we are currently connected to amqp before signalling success with some APIs, such as 'create session' @@ -54,7 +54,7 @@ module JamWebEventMachine end end - calling_thread.wakeup + calling_thread.wakeup if calling_thread end end @@ -65,6 +65,7 @@ module JamWebEventMachine end def self.run + return if defined?(Rails::Console) current = Thread.current Thread.new do run_em(current) diff --git a/ruby/lib/jam_ruby/lib/google_analytics_tracker.rb b/ruby/lib/jam_ruby/lib/google_analytics_tracker.rb index 85cb240c0..3603b4960 100644 --- a/ruby/lib/jam_ruby/lib/google_analytics_tracker.rb +++ b/ruby/lib/jam_ruby/lib/google_analytics_tracker.rb @@ -1,5 +1,6 @@ require 'rest_client' +# more info on Measurement Protocol https://developers.google.com/analytics/devguides/collection/protocol/v1/ class GoogleAnalyticsTracker attr_accessor :enabled, :tracking_code, :ga_version, :ga_endpoint diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 491e1b3e5..fbcb1673d 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -580,6 +580,23 @@ module JamRuby ) end + # creates the session chat message + def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at) + chat_message = Jampb::ChatMessage.new( + :sender_id => sender_id, + :sender_name => sender_name, + :msg => msg, + :msg_id => msg_id, + :created_at => created_at + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::CHAT_MESSAGE, + :route_to => SESSION_TARGET_PREFIX + session_id, + :chat_message => chat_message + ) + end + # create a musician fresh session message def musician_session_fresh(session_id, user_id, username, photo_url) fresh = Jampb::MusicianSessionFresh.new( diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb new file mode 100644 index 000000000..6df9dc1a2 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -0,0 +1,44 @@ +class JamRuby::AffiliatePartner < ActiveRecord::Base + belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id + has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id + + attr_accessible :partner_name, :partner_code, :partner_user_id + + PARAM_REFERRAL = :ref + PARAM_COOKIE = :affiliate_ref + + PARTNER_CODE_REGEX = /^[#{Regexp.escape('abcdefghijklmnopqrstuvwxyz0123456789-._+,')}]+{2,128}$/i + + validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email + validates :partner_name, presence: true + validates :partner_code, presence: true, format: { with: PARTNER_CODE_REGEX } + validates :partner_user, presence: true + + def self.create_with_params(params={}) + oo = self.new + oo.partner_name = params[:partner_name].try(:strip) + oo.partner_code = params[:partner_code].try(:strip).try(:downcase) + oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first + oo.partner_user_id = oo.partner_user.try(:id) + oo.save + oo + end + + def self.coded_id(code=nil) + self.where(:partner_code => code).limit(1).pluck(:id).first if code.present? + end + + def self.is_code?(code) + self.where(:partner_code => code).limit(1).pluck(:id).present? + end + + def referrals_by_date + by_date = User.where(:affiliate_referral_id => self.id) + .group('DATE(created_at)') + .having("COUNT(*) > 0") + .order('date_created_at DESC') + .count + block_given? ? yield(by_date) : by_date + end + +end diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb new file mode 100644 index 000000000..786b48642 --- /dev/null +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -0,0 +1,27 @@ +module JamRuby + class ChatMessage < ActiveRecord::Base + + self.primary_key = 'id' + + default_scope order('created_at DESC') + + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" + belongs_to :session, :class_name => "JamRuby::MusicSession", :foreign_key => "session_id" + + validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true + + def self.send_chat_msg(music_session, chat_msg, user) + msg = @@message_factory.chat_message( + music_session.id, + user.name, + user.id, + chat_msg.message, + chat_msg.id, + chat_msg.created_at + ) + + @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => chat_msg.user_id}) + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 77966d5cc..87e8ee49c 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -99,6 +99,10 @@ module JamRuby # events has_many :event_sessions, :class_name => "JamRuby::EventSession" + # affiliate_partner + has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id + belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count + # This causes the authenticate method to be generated (among other stuff) #has_secure_password @@ -735,6 +739,7 @@ module JamRuby invited_user = options[:invited_user] fb_signup = options[:fb_signup] signup_confirm_url = options[:signup_confirm_url] + affiliate_referral_id = options[:affiliate_referral_id] user = User.new @@ -836,6 +841,10 @@ module JamRuby if user.errors.any? raise ActiveRecord::Rollback else + if user.affiliate_referral = AffiliatePartner.find_by_id(affiliate_referral_id) + user.save + end if affiliate_referral_id.present? + # don't send an signup email if email is already confirmed if user.email_confirmed UserMailer.welcome_message(user).deliver diff --git a/ruby/lib/jam_ruby/mq_router.rb b/ruby/lib/jam_ruby/mq_router.rb index 78c8dd800..8b6063f0e 100644 --- a/ruby/lib/jam_ruby/mq_router.rb +++ b/ruby/lib/jam_ruby/mq_router.rb @@ -55,6 +55,7 @@ class MQRouter # sends a message to a client with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_client(client_id, client_msg, sender = {:client_id => ""}) + @@log.error "EM not running in publish_to_client" unless EM.reactor_running? EM.schedule do sender_client_id = sender[:client_id] @@ -68,6 +69,7 @@ class MQRouter # sends a message to a session with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => nil}) + @@log.error "EM not running in publish_to_session" unless EM.reactor_running? EM.schedule do sender_client_id = sender[:client_id] @@ -84,7 +86,7 @@ class MQRouter # sends a message to a user with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_user(user_id, user_msg) - @@log.warn "EM not running in publish_to_user" unless EM.reactor_running? + @@log.error "EM not running in publish_to_user" unless EM.reactor_running? EM.schedule do @@log.debug "publishing to user:#{user_id} from server" @@ -96,6 +98,8 @@ class MQRouter # sends a message to a list of friends with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_friends(friend_ids, user_msg, from_user_id) + @@log.error "EM not running in publish_to_friends" unless EM.reactor_running? + EM.schedule do friend_ids.each do |friend_id| @@log.debug "publishing to friend:#{friend_id} from user/band #{from_user_id}" diff --git a/ruby/lib/jam_ruby/resque/google_analytics_event.rb b/ruby/lib/jam_ruby/resque/google_analytics_event.rb index deb85cde1..58c393084 100644 --- a/ruby/lib/jam_ruby/resque/google_analytics_event.rb +++ b/ruby/lib/jam_ruby/resque/google_analytics_event.rb @@ -1,5 +1,6 @@ require 'resque' +# more info on Measurement Protocol https://developers.google.com/analytics/devguides/collection/protocol/v1/ module JamRuby class GoogleAnalyticsEvent diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb new file mode 100644 index 000000000..8470f8cee --- /dev/null +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe AffiliatePartner do + + let!(:user) { FactoryGirl.create(:user) } + let!(:partner) { + AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, + :partner_code => Faker::Lorem.word, + :user_email => user.email}) + } + + it 'validates required fields' do + pending + expect(partner.referral_user_count).to eq(0) + expect(partner.partner_user).to eq(user) + user.reload + expect(user.affiliate_partner).to eq(partner) + + oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, + :partner_code => 'a', + :user_email => user.email}) + expect(oo.errors.messages[:partner_code][0]).to eq('is invalid') + oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, + :partner_code => 'foo bar', + :user_email => user.email}) + expect(oo.errors.messages[:partner_code][0]).to eq('is invalid') + oo = AffiliatePartner.create_with_params({:partner_name => '', + :partner_code => Faker::Lorem.word, + :user_email => user.email}) + expect(oo.errors.messages[:partner_name][0]).to eq("can't be blank") + oo = AffiliatePartner.create_with_params({:partner_name => '', + :partner_code => Faker::Lorem.word, + :user_email => Faker::Internet.email}) + expect(oo.errors.messages[:partner_user][0]).to eq("can't be blank") + + code = Faker::Lorem.word.upcase + oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, + :partner_code => " #{code} ", + :user_email => user.email}) + expect(oo.partner_code).to eq(code.downcase) + end + + it 'has user referrals' do + pending + expect(AffiliatePartner.coded_id(partner.partner_code)).to eq(partner.id) + expect(partner.referral_user_count).to eq(0) + uu = FactoryGirl.create(:user) + uu.affiliate_referral = partner + uu.save + partner.reload + expect(uu.affiliate_referral).to eq(partner) + expect(partner.referral_user_count).to eq(1) + expect(partner.user_referrals[0]).to eq(uu) + end + + it 'groups referrals properly' do + FactoryGirl.create(:user, :created_at => Time.now - 7.days, :affiliate_referral_id => partner.id) + FactoryGirl.create(:user, :created_at => Time.now - 7.days, :affiliate_referral_id => partner.id) + FactoryGirl.create(:user, :created_at => Time.now - 6.days, :affiliate_referral_id => partner.id) + FactoryGirl.create(:user, :created_at => Time.now - 6.days, :affiliate_referral_id => partner.id) + FactoryGirl.create(:user, :created_at => Time.now - 3.days, :affiliate_referral_id => partner.id) + FactoryGirl.create(:user, :created_at => Time.now - 2.days, :affiliate_referral_id => partner.id) + partner.reload + expect(partner.referral_user_count).to eq(6) + + by_date = partner.referrals_by_date + expect(by_date.count).to eq(4) + keys = by_date.keys + expect(Date.parse(keys.first)).to eq(Date.parse((Time.now - 2.days).to_s)) + expect(by_date[keys.first]).to eq(1) + expect(Date.parse(keys.last)).to eq(Date.parse((Time.now - 7.days).to_s)) + expect(by_date[keys.last]).to eq(2) + end + +end diff --git a/web/app/assets/images/content/icon_dollar.png b/web/app/assets/images/content/icon_dollar.png new file mode 100644 index 000000000..7d6c1aac2 Binary files /dev/null and b/web/app/assets/images/content/icon_dollar.png differ diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 04bd51344..3f974b0a2 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -51,6 +51,7 @@ // text message TEXT_MESSAGE : "TEXT_MESSAGE", + CHAT_MESSAGE : "CHAT_MESSAGE", // broadcast notifications SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED", diff --git a/web/app/assets/javascripts/affiliate_report.js b/web/app/assets/javascripts/affiliate_report.js new file mode 100644 index 000000000..662611131 --- /dev/null +++ b/web/app/assets/javascripts/affiliate_report.js @@ -0,0 +1,65 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AffiliateReportScreen = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var user = {}; + + function beforeShow(data) { + } + + function afterShow(data) { + renderAffiliateReport(); + } + + function populateAffiliateReport(report) { + console.log(report); + var by_date = ''; + var ii=0, dates_len = report['by_date'].length; + for (var ii=0; ii < dates_len; ii += 1) { + var dd = report['by_date'][ii]; + by_date += '