Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop

This commit is contained in:
Seth Call 2014-04-15 15:02:57 +00:00
commit 21a101cc56
16 changed files with 415 additions and 41 deletions

View File

@ -142,3 +142,4 @@ order_event_session.sql
emails.sql
email_batch.sql
user_progress_tracking2.sql
bands_did_session.sql

View File

@ -0,0 +1,2 @@
ALTER TABLE bands ADD COLUMN did_real_session boolean default false;

View File

@ -54,7 +54,8 @@ group :test do
gem 'spork', '0.9.0'
gem 'database_cleaner', '0.7.0'
gem 'faker'
gem 'resque_spec'
gem 'resque_spec', :path => "/home/jam/src/resque_spec/"
gem 'timecop'
end
# Specify your gem's dependencies in jam_ruby.gemspec

View File

@ -40,6 +40,7 @@ require "jam_ruby/resque/scheduled/audiomixer_retry"
require "jam_ruby/resque/scheduled/icecast_config_retry"
require "jam_ruby/resque/scheduled/icecast_source_check"
require "jam_ruby/resque/scheduled/cleanup_facebook_signup"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/mq_router"
require "jam_ruby/base_manager"
require "jam_ruby/connection_manager"

View File

@ -263,6 +263,12 @@ module JamRuby
name
end
def in_real_session?(session)
b_members = self.users.sort_by(&:id).map(&:id)
s_members = session.users.sort_by(&:id).map(&:id)
(b_members - s_members).blank?
end
private
def require_at_least_one_genre

View File

@ -18,6 +18,7 @@ module JamRuby
validate :can_join_music_session, :if => :joining_session?
after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
after_create :did_create
after_save :report_add_participant
include AASM
IDLE_STATE = :idle
@ -127,6 +128,17 @@ module JamRuby
self.user.update_lat_lng(self.ip_address) if self.user && self.ip_address
end
def report_add_participant
if self.music_session_id_changed? &&
self.music_session.present? &&
self.connected? &&
self.as_musician? &&
0 < (count = self.music_session.connected_participant_count)
GoogleAnalyticsEvent.report_session_participant(count)
end
true
end
private
def require_at_least_one_track_when_in_session
if tracks.count == 0

View File

@ -24,6 +24,8 @@ module JamRuby
has_many :recordings, :class_name => "JamRuby::Recording", :inverse_of => :music_session
belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id"
after_create :started_session
validate :require_at_least_one_genre, :limit_max_genres
after_save :sync_music_session_history
@ -401,6 +403,18 @@ module JamRuby
self.save!(:validate => false)
end
def connected_participant_count
Connection.where(:music_session_id => self.id,
:aasm_state => Connection::CONNECT_STATE.to_s,
:as_musician => true)
.count
end
def started_session
GoogleAnalyticsEvent.track_session_duration(self)
GoogleAnalyticsEvent.track_band_real_session(self)
end
private
def require_at_least_one_genre

View File

@ -146,6 +146,8 @@ module JamRuby
recording.band = music_session.band
if recording.save
GoogleAnalyticsEvent.report_band_recording(recording.band)
music_session.connections.each do |connection|
connection.tracks.each do |track|
recording.recorded_tracks << RecordedTrack.create_from_track(track, recording)

View File

@ -1,43 +1,107 @@
class GoogleAnalyticsEvent
require 'resque'
@queue = 'google_analytics_event'
module JamRuby
class GoogleAnalyticsEvent
@@log = Logging.logger[GoogleAnalyticsEvent]
@queue = :google_analytics_event
def self.perform(category, action)
CAT_SESS_SIZE = 'SessionSize'
ACTION_SESS_SIZE = 'Size'
CAT_SESS_DUR = 'SessionDuration'
ACTION_SESS_DUR = 'Duration'
CAT_BAND = 'Band'
ACTION_BAND_SESS = 'Session'
ACTION_BAND_REC = 'Recording'
@@log.info("starting (#{category}, #{action})")
@@log = Logging.logger[GoogleAnalyticsEvent]
run(category, action)
SESSION_INTERVALS = [1, 5, 10, 15, 30, 45, 60, 90, 120, 180] # minutes
QUEUE_BAND_TRACKER = :band_tracker
QUEUE_SESSION_TRACKER = :session_tracker
@@log.info("done (#{category}, #{action})")
class SessionDurationTracker
@queue = QUEUE_SESSION_TRACKER
end
def self.enqueue(category, event)
begin
Resque.enqueue(AudioMixer, category, event)
true
rescue
# implies redis is down. but since there is no retry logic with this, we should at least log a warn in case we've configured something wrong
@@log.warn("unable to enqueue")
false
def self.perform(args={})
session_id, interval_idx = args['session_id'], args['interval_idx'].to_i
return unless session_id && session = MusicSession.find(session_id)
GoogleAnalyticsEvent.enqueue(CAT_SESS_DUR, ACTION_SESS_DUR, SESSION_INTERVALS[interval_idx])
interval_idx += 1
if SESSION_INTERVALS.count-1 > interval_idx
next_time = session.created_at + SESSION_INTERVALS[interval_idx].minutes
Resque.enqueue_at(next_time, self, :session_id => session_id, :interval_idx => interval_idx)
end
end
end
end
def self.run(category, action)
def self.track_session_duration(session)
Resque.enqueue_at(SESSION_INTERVALS[0].minute.from_now,
SessionDurationTracker,
:session_id => session.id,
:interval_idx => 0)
end
raise "no google analytics tracking ID" unless APP_CONFIG.ga_ua
class BandSessionTracker
@queue = QUEUE_BAND_TRACKER
params = {
def self.perform(session_id)
return unless session = MusicSession.find(session_id)
band = session.band
if band.in_real_session?(session)
band.update_attribute(:did_real_session, true)
GoogleAnalyticsEvent.enqueue(CAT_BAND, ACTION_BAND_SESS, nil)
end if band
end
end
BAND_SESSION_MIN_DURATION = 15 # minutes
def self.track_band_real_session(session)
if session.band && !session.band.did_real_session?
Resque.enqueue_at(BAND_SESSION_MIN_DURATION.minutes.from_now,
BandSessionTracker,
session.id)
end
end
def self.report_band_recording(band)
if band && 1 == Recording.where(:band_id => band.id).count
self.enqueue(CAT_BAND, ACTION_BAND_REC)
end
end
def self.report_session_participant(participant_count)
self.enqueue(CAT_SESS_SIZE, ACTION_SESS_SIZE, participant_count)
end
def self.enqueue(category, event, data=nil)
begin
Resque.enqueue(GoogleAnalyticsEvent, category, event, data)
true
rescue
# implies redis is down. but since there is no retry logic with this, we should at least log a warn in case we've configured something wrong
@@log.warn("unable to enqueue")
false
end
end
def self.perform(category, action, data)
@@log.info("starting (#{category}, #{action})")
raise "no google analytics tracking ID" unless APP_CONFIG.ga_ua
params = {
v: APP_CONFIG.ga_ua_version,
tid: APP_CONFIG.ga_ua,
cid: APP_CONFIG.ga_anonymous_client_id,
t: "event",
ec: category,
ea: action
}
ea: action,
el: 'data',
ev: data.to_s
}
RestClient.post(APP_CONFIG.ga_endpoint, params: params, timeout: 8, open_timeout: 8)
@@log.info("done (#{category}, #{action})")
end
RestClient.post(APP_CONFIG.ga_endpoint, params: params, timeout: 8, open_timeout: 8)
end
end
end

View File

@ -160,6 +160,7 @@ FactoryGirl.define do
association :owner, factory: :user
association :music_session, factory: :music_session
association :band, factory: :band
factory :recording_with_track do
before(:create) { |recording|

View File

@ -0,0 +1,105 @@
require 'spec_helper'
describe GoogleAnalyticsEvent do
let(:ga) { GoogleAnalyticsEvent.new }
describe "track band analytics" do
it 'reports first recording' do
ResqueSpec.reset!
user = FactoryGirl.create(:user)
band = FactoryGirl.create(:band)
music_session = FactoryGirl.create(:music_session,
:creator => user,
:musician_access => true,
:band => band)
recording = Recording.start(music_session, user)
expect(Recording.where(:band_id => band.id).count).to eq(1)
GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_BAND,
GoogleAnalyticsEvent::ACTION_BAND_REC,
nil)
end
it 'reports first real session' do
ResqueSpec.reset!
JamRuby::GoogleAnalyticsEvent::BandSessionTracker.should have_schedule_size_of(0)
user = FactoryGirl.create(:user)
user1 = FactoryGirl.create(:user)
band = FactoryGirl.create(:band)
band.users << user
band.users << user1
band.reload
music_session = FactoryGirl.create(:music_session, :creator => user,
:musician_access => true, :band => band)
expect(band.band_musicians.count).to eq(2)
expect(band.did_real_session).to eq(false)
connection = FactoryGirl.create(:connection, :user => user, :as_musician => true,
:aasm_state => Connection::CONNECT_STATE.to_s,
:music_session => music_session)
connection = FactoryGirl.create(:connection, :user => user1, :as_musician => true,
:aasm_state => Connection::CONNECT_STATE.to_s,
:music_session => music_session)
music_session.reload
expect(music_session.connected_participant_count).to eq(2)
expect(band.did_real_session).to eq(false)
ResqueSpec.queues["#{GoogleAnalyticsEvent::QUEUE_BAND_TRACKER}_scheduled"].select do |qq|
qq[:class] == GoogleAnalyticsEvent::BandSessionTracker.name
end.count.should eq(1)
# GoogleAnalyticsEvent::BandSessionTracker.should have_schedule_size_of_at_least(1)
GoogleAnalyticsEvent.should_not have_queued(GoogleAnalyticsEvent::CAT_BAND, GoogleAnalyticsEvent::ACTION_BAND_SESS, nil)
Timecop.freeze((GoogleAnalyticsEvent::BAND_SESSION_MIN_DURATION + 1).minutes.from_now)
qname = "#{ResqueSpec.queue_name(JamRuby::GoogleAnalyticsEvent::BandSessionTracker)}_scheduled"
expect(ResqueSpec.peek(qname).present?).to eq(true)
ResqueSpec.perform_next(qname)
GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_BAND,
GoogleAnalyticsEvent::ACTION_BAND_SESS,
nil)
band.reload
expect(band.did_real_session).to eq(true)
end
end
describe "track session analytics" do
before :each do
ResqueSpec.reset!
end
it 'reports size increment' do
user = FactoryGirl.create(:user)
music_session = FactoryGirl.create(:music_session,
:creator => user,
:musician_access => true)
connection = FactoryGirl.create(:connection, :user => user,
:as_musician => true,
:aasm_state => Connection::CONNECT_STATE.to_s,
:music_session => music_session)
GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_SESS_SIZE,
GoogleAnalyticsEvent::ACTION_SESS_SIZE,
music_session.connected_participant_count)
end
it 'reports duration' do
user = FactoryGirl.create(:user)
JamRuby::GoogleAnalyticsEvent::SessionDurationTracker.should have_schedule_size_of(0)
music_session = FactoryGirl.create(:music_session,
:creator => user,
:musician_access => true)
GoogleAnalyticsEvent::SessionDurationTracker.should have_schedule_size_of(1)
GoogleAnalyticsEvent::SESSION_INTERVALS.each do |interval|
Timecop.travel((interval + 1).minutes.from_now)
qname = "#{ResqueSpec.queue_name(JamRuby::GoogleAnalyticsEvent::SessionDurationTracker)}_scheduled"
next unless ResqueSpec.peek(qname).present?
ResqueSpec.perform_next(qname)
GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_SESS_DUR,
GoogleAnalyticsEvent::ACTION_SESS_DUR,
interval)
end
GoogleAnalyticsEvent.should have_queue_size_of(GoogleAnalyticsEvent::SESSION_INTERVALS.count - 1)
end
end
end

View File

@ -26,6 +26,9 @@ require 'spork'
require 'database_cleaner'
require 'factories'
require 'timecop'
require 'resque_spec/scheduler'
# uncomment this to see active record logs
#ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)
@ -68,6 +71,7 @@ Spork.prefork do
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
config.color_enabled = true
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true
@ -105,6 +109,32 @@ Spork.prefork do
# the seed, which is printed after each run.
# --seed 1234
config.order = 'random'
REDIS_PID = "#{Rails.root}/tmp/pids/redis-test.pid"
REDIS_CACHE_PATH = "#{Rails.root}/tmp/cache/"
config.before(:suite) do
redis_options = {
"--daemonize" => 'yes',
"--pidfile" => REDIS_PID,
"--port" => 9736,
"--timeout" => 300,
"--save 900" => 1,
"--save 300" => 1,
"--save 60" => 10000,
"--dbfilename" => "dump.rdb",
"--dir" => REDIS_CACHE_PATH,
"--loglevel" => "debug",
"--logfile" => "stdout",
"--databases" => 16
}.map { |k, v| "#{k} #{v}" }.join(" ")
`redis-server #{redis_options}`
end
config.after(:suite) do
%x{
cat #{REDIS_PID} | xargs kill -QUIT
rm -f #{REDIS_CACHE_PATH}dump.rdb
}
end
end
end

View File

@ -73,7 +73,7 @@
instrumentHtml = '<td><div class="nowrap">';
$.each(val.instrument_ids, function(index, val) {
instrumentHtml += '<img src="' + context.JK.getInstrumentIcon24(val) + '" width="24" height="24" />&nbsp;&nbsp;';
})
});
instrumentHtml += '</div></td>';
musicianHtml += instrumentHtml;

View File

@ -4,7 +4,7 @@
background-color:#242323;
border:solid 1px #ed3618;
position:absolute;
z-index:999;
z-index:1100;
&.musician-bubble {

View File

@ -16,16 +16,16 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do
#end
end
let(:fan) { FactoryGirl.create(:fan) }
let(:user) { FactoryGirl.create(:user) }
let(:finder) { FactoryGirl.create(:user) }
before(:each) do
UserMailer.deliveries.clear
navigate_band_setup
end
def navigate_band_setup
sign_in_poltergeist(user)
def navigate_band_setup login=user
sign_in_poltergeist(login)
wait_until_curtain_gone
find('div.homecard.profile').trigger(:click)
find('#profile-bands-link').trigger(:click)
@ -33,26 +33,143 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do
expect(page).to have_selector('#band-setup-title')
end
it "have validation errors shown, but then can navigate past and eventually save" do
find('#btn-band-setup-next').trigger(:click)
find('#tdBandName .error-text li', text: "can't be blank")
find('#tdBandBiography .error-text li', text: "can't be blank")
find('#tdBandGenres .error-text li', text: "At least 1 genre is required.")
def complete_band_setup_form(band, biography, params={})
navigate_band_setup unless URI.parse(current_url).fragment == '/band/setup/new'
params['band-name'] ||= band || "Default band name"
params['band-biography'] ||= biography || "Default band biography"
within('#band-setup-form') do
fill_in "band-name", with: "The Band"
fill_in "band-biography", with: "Biography"
params.each do |field, value|
fill_in field, with: "#{value}"
end
first('#band-genres input[type="checkbox"]').trigger(:click)
end
sleep 1 # work around race condition
find('#btn-band-setup-next').trigger(:click)
find('h2', text: 'Step 2: Add Band Members')
find('#btn-band-setup-save').trigger(:click)
find('#band-profile-name', text: "The Band")
find('#band-profile-biography', text: "Biography")
end
context "band profile - new band setup" do
it "displays 'Set up your band' link to user" do
sign_in_poltergeist user
view_profile_of user
find('#profile-bands-link').trigger(:click)
expect(page).to have_selector('#band-setup-link')
end
it "does not display band setup link when viewed by other user" do
in_client(fan) do
sign_in_poltergeist fan
view_profile_of user
find('#profile-bands-link').trigger(:click)
expect(page).to_not have_selector('#band-setup-link')
end
end
it "indicates required fields and user may eventually complete" do
navigate_band_setup
find('#btn-band-setup-next').trigger(:click)
expect(page).to have_selector('#tdBandName .error-text li', text: "can't be blank")
expect(page).to have_selector('#tdBandBiography .error-text li', text: "can't be blank")
expect(page).to have_selector('#tdBandGenres .error-text li', text: "At least 1 genre is required.")
complete_band_setup_form("Band name", "Band biography")
expect(page).to have_selector('#band-profile-name', text: "Band name")
expect(page).to have_selector('#band-profile-biography', text: "Band biography")
end
it "limits genres to 3" do
navigate_band_setup
within('#band-setup-form') do
fill_in 'band-name', with: "whatever"
fill_in 'band-biography', with: "a good story"
all('#band-genres input[type="checkbox"]').each_with_index do |cb, i|
cb.trigger(:click) unless i > 3
end
end
sleep 1
find('#btn-band-setup-next').trigger(:click)
expect(page).to have_selector('#tdBandGenres .error-text li', text: "No more than 3 genres are allowed.")
end
it "handles max-length field input" do
pending "update this after VRFS-1610 is resolved"
max = {
name: 1024,
bio: 4000,
website: 1024 # unsure what the max is, see VRFS-1610
}
navigate_band_setup
band_name = 'a'*(max[:name] + 1)
band_bio = 'b'*(max[:bio] + 1)
band_website = 'c'*(max[:website] + 1)
complete_band_setup_form(band_name, band_bio, 'band-website' => band_website)
expect(page).to have_selector('#band-profile-name', text: band_name.slice(0, max[:name]))
expect(page).to have_selector('#band-profile-biography', text: band_bio.slice(0, max[:bio]))
end
it "handles special characters in text fields" do
pending "update this after VRFS-1609 is resolved"
navigate_band_setup
band_name = garbage(3) + ' ' + garbage(50)
band_bio = garbage(500)
band_website = garbage(500)
complete_band_setup_form(band_name, band_bio, 'band-website' => band_website)
expect(page).to have_selector('#band-profile-name', text: band_name)
expect(page).to have_selector('#band-profile-biography', text: band_bio)
end
it "another user receives invite notification during Band Setup"
end
context "about view" do
it "displays the band's information to another user"
#photo
#name
#website address
#country, state, city
#biography/description
#genres chosen
#number of followers, recordings, sessions
#actions: follow button
it "allows a user to follow the band"
end
context "members view" do
it "photo and name links to the musician's profile page"
it "displays photo, name, location, instruments played"
it "displays a hover bubble containing more info on musician"
it "displays any pending band invitations when viewed by current band member"
end
context "history view" do
it "shows public info"
it "does not show private info to non-band user"
it "shows private info to band user"
end
context "social view" do
it "displays musicians and fans who follow band"
end
context "band profile - editing" do
it "about page shows the current band's info when 'Edit Profile' is clicked"
it "members page shows 'Edit Members' button and user can remove member"
it "non-member cannot Edit Profile"
it "non-member cannot Edit Members"
end
it "band shows up in sidebar search result"
end

View File

@ -415,6 +415,13 @@ def assert_all_tracks_seen(users=[])
end
end
def view_profile_of user
id = user.kind_of?(JamRuby::User) ? user.id : user
# assume some user signed in already (allows reuse in multi-user tests)
visit "/client#/profile/#{id}"
wait_until_curtain_gone
end
def show_user_menu
page.execute_script("$('ul.shortcuts').show()")
#page.execute_script("JK.UserDropdown.menuHoverIn()")
@ -430,4 +437,15 @@ def send_key(selector, keycode = 13)
keypress_script = "var e = $.Event('keyup', { keyCode: #{keycode} }); jQuery('#{selector}').trigger(e);"
page.driver.execute_script(keypress_script)
end
def special_characters
["?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}"]
end
def garbage length
output = ''
length.times { output << special_characters.sample }
output.gsub!(/<[\/|!|\?]/, '/<') # security risk -- avoids inputting tags until VRFS-1609 resolved
output.slice(0, length)
end