diff --git a/db/manifest b/db/manifest index 9808e5748..04ad4b590 100755 --- a/db/manifest +++ b/db/manifest @@ -72,3 +72,4 @@ isp_score_batch.sql crash_dumps.sql crash_dumps_idx.sql music_sessions_user_history_add_session_removed_at.sql +user_progress_tracking.sql diff --git a/db/up/user_progress_tracking.sql b/db/up/user_progress_tracking.sql new file mode 100644 index 000000000..09c06fb75 --- /dev/null +++ b/db/up/user_progress_tracking.sql @@ -0,0 +1,15 @@ +-- tracks how users are progessing through the site: https://jamkazam.atlassian.net/wiki/pages/viewpage.action?pageId=3375145 + +alter table music_sessions_user_history add column max_concurrent_connections integer; +alter table music_sessions_user_history add column rating integer; +alter table users add column last_failed_certified_gear_at TIMESTAMP; +alter table users add column last_failed_certified_gear_reason varchar(256); +alter table users add column first_downloaded_client_at TIMESTAMP; +alter table users add column first_ran_client_at TIMESTAMP; +alter table users add column first_certified_gear_at TIMESTAMP; +alter table users add column first_music_session_at TIMESTAMP; +alter table users add column first_real_music_session_at TIMESTAMP; +alter table users add column first_good_music_session_at TIMESTAMP; +alter table users add column first_invited_at TIMESTAMP; +alter table users add column first_friended_at TIMESTAMP; +alter table users add column first_social_promoted_at TIMESTAMP; diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 1161e14b4..e79ee8b29 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -263,8 +263,7 @@ SQL if result.cmd_tuples == 1 # music session deleted! @log.debug("deleted music session #{previous_music_session_id}") - JamRuby::MusicSessionHistory.removed_music_session(user_id, - previous_music_session_id) + JamRuby::MusicSessionHistory.removed_music_session(previous_music_session_id) elsif 1 < result.cmd_tuples msg = "music_sessions table data integrity violation; multiple rows found with music_session_id=#{previous_music_session_id}" @log.error(msg) @@ -296,6 +295,7 @@ SQL raise ActiveRecord::Rollback else blk.call(db_conn, connection) unless blk.nil? + user.update_progression_field(:first_music_session_at) MusicSessionUserHistory.save(music_session_id, user_id, client_id) end end diff --git a/ruby/lib/jam_ruby/models/friendship.rb b/ruby/lib/jam_ruby/models/friendship.rb index 52132d889..4a175e81d 100644 --- a/ruby/lib/jam_ruby/models/friendship.rb +++ b/ruby/lib/jam_ruby/models/friendship.rb @@ -8,6 +8,8 @@ module JamRuby belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_friendships belongs_to :friend, :class_name => "JamRuby::User", :foreign_key => "friend_id", :inverse_of => :friendships + after_save :track_user_progression + def self.save(user_id, friend_id) friendship = Friendship.where("user_id='#{user_id}' AND friend_id='#{friend_id}'") @@ -60,5 +62,9 @@ module JamRuby friends = friends.limit(options[:limit]) return friends end + + def track_user_progression + self.user.update_progression_field(:first_friended_at) + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/invited_user.rb b/ruby/lib/jam_ruby/models/invited_user.rb index 47069c43e..059e2170e 100644 --- a/ruby/lib/jam_ruby/models/invited_user.rb +++ b/ruby/lib/jam_ruby/models/invited_user.rb @@ -22,12 +22,18 @@ module JamRuby validate :not_accepted_twice validate :can_invite? + after_save :track_user_progression + # ensure invitation code is always created before_validation(:on => :create) do self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil? self.sender_id = nil if self.sender_id.blank? # this coercion was done just to make activeadmin work end + def track_user_progression + self.sender.update_progression_field(:first_invited_at) unless self.sender.nil? + end + def self.index(user) return InvitedUser.where(:sender_id => user).order(:updated_at) end @@ -60,7 +66,5 @@ module JamRuby def not_accepted_twice errors.add(:accepted, "you can only accept an invitation once") if accepted_twice end - - end end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index d3c7660cc..c61246b53 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -24,7 +24,7 @@ module JamRuby after_save :require_at_least_one_genre, :limit_max_genres after_destroy do |obj| - JamRuby::MusicSessionHistory.removed_music_session(obj.user_id, obj.id) + JamRuby::MusicSessionHistory.removed_music_session(obj.id) end validates :description, :presence => true, :no_profanity => true diff --git a/ruby/lib/jam_ruby/models/music_session_history.rb b/ruby/lib/jam_ruby/models/music_session_history.rb index 0165476d8..f87154c31 100644 --- a/ruby/lib/jam_ruby/models/music_session_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_history.rb @@ -67,14 +67,29 @@ module JamRuby session_history.save! end - def self.removed_music_session(user_id, session_id) + def end_history + self.update_attribute(:session_removed_at, Time.now) + + + # ensure all user histories are closed + music_session_user_histories.each do |music_session_user_history| + music_session_user_history.end_history + + # then update any users that need their user progress updated + if music_session_user_history.duration_minutes > 15 && music_session_user_history.max_concurrent_connections >= 3 + music_session_user_history.user.update_progression_field(:first_real_music_session_at) + end + end + + end + + def self.removed_music_session(session_id) hist = self - .where(:user_id => user_id) .where(:music_session_id => session_id) .limit(1) .first - hist.update_attribute(:session_removed_at, Time.now) if hist - JamRuby::MusicSessionUserHistory.removed_music_session(user_id, session_id) + + hist.end_history if hist end def duration_minutes diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index 080298faa..5b9fcf258 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -5,11 +5,16 @@ module JamRuby self.primary_key = 'id' + attr_accessible :max_concurrent_connections, :session_removed_at, :rating + belongs_to(:user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :music_session_user_histories) + validates_inclusion_of :rating, :in => 0..2, :allow_nil => true + after_save :track_user_progression + def music_session_history @msh ||= JamRuby::MusicSessionHistory.find_by_music_session_id(self.music_session_id) end @@ -41,12 +46,60 @@ module JamRuby .where(:music_session_id => session_id) .limit(1) .first - hist.update_attribute(:session_removed_at, Time.now) if hist + hist.end_history if hist + end + + def end_history + self.session_removed_at = Time.now if self.session_removed_at.nil? + + self.update_attributes(:session_removed_at => self.session_removed_at, :max_concurrent_connections => determine_max_concurrent) + end + + # figures out what the peak amount of other clients the user saw while playing at any given time. + # we use the database to get all other connections that occurred while their this user was connected, + # and then sort through them using custom logic to increase/decrease the count as people come and go + def determine_max_concurrent + overlapping_connections = MusicSessionUserHistory.where("music_session_id = ? AND + ((created_at >= ? AND (session_removed_at is NULL OR session_removed_at <= ?)) OR + (created_at <= ? AND (session_removed_at is NULL OR session_removed_at >= ?)))", + self.music_session_id, self.created_at, self.session_removed_at, self.created_at, self.created_at).select('id, created_at, session_removed_at').order(:created_at) + in_out_times = [] + overlapping_connections.each do |connection| + in_out_times.push([connection.created_at.nil? ? Time.at(0) : connection.created_at, true]) + in_out_times.push([connection.session_removed_at.nil? ? Time.new(3000) : connection.session_removed_at, false]) #helpful to get rid of nulls for sorting + end + + count_concurrent(in_out_times) + end + + def count_concurrent(in_out_times) + in_out_times.sort! + + max_concurrent = 0 + concurrent = 0 + + in_out_times.each do |hit| + if hit[1] + concurrent = concurrent + 1 + else + concurrent = concurrent - 1 + end + + if concurrent > max_concurrent + max_concurrent = concurrent + end + end + max_concurrent end def perf_uri self.perf_data.try(:uri) end - + + def track_user_progression + if self.rating == 0 + user.update_progression_field(:first_good_music_session_at) + end + end end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0e2c4d1ac..14133dc49 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -12,7 +12,7 @@ module JamRuby attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_s3_path, :photo_url, :crop_selection # updating_password corresponds to a lost_password - attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar + attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" @@ -133,6 +133,24 @@ module JamRuby validate :email_case_insensitive_uniqueness validate :update_email_case_insensitive_uniqueness, :if => :updating_email + def user_progression_fields + @user_progression_fields ||= Set.new ["first_downloaded_client_at", "first_ran_client_at", "first_music_session_at", "first_real_music_session_at", "first_good_music_session_at", "first_certified_gear_at", "first_invited_at", "first_friended_at", "first_social_promoted_at" ] + end + + def update_progression_field(field_name, time = DateTime.now) + @updating_progression_field = true + if self[field_name].nil? + self[field_name] = time + self.save + end + end + + def failed_qualification(reason) + self.last_failed_certified_gear_at = DateTime.now + self.last_failed_certified_gear_reason = reason + self.save + end + def validate_musician_instruments errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_MINIMUM_NOT_MET) if !administratively_created && musician && musician_instruments.length == 0 errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_LIMIT_EXCEEDED) if !administratively_created && musician && musician_instruments.length > 5 diff --git a/ruby/spec/jam_ruby/models/friendship_spec.rb b/ruby/spec/jam_ruby/models/friendship_spec.rb new file mode 100644 index 000000000..ba3a42914 --- /dev/null +++ b/ruby/spec/jam_ruby/models/friendship_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Friendship do + + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + + before(:each) do + Friendship.save_using_models(user1, user2) + end + + it "can create two-way friendship" do + user1.friends?(user2).should be_true + user2.friends?(user1).should be_true + end + + it "should track user progression" do + user1.first_friended_at.should_not be_nil + user2.first_friended_at.should_not be_nil + end + + +end diff --git a/ruby/spec/jam_ruby/models/invited_user_spec.rb b/ruby/spec/jam_ruby/models/invited_user_spec.rb index b3de880b5..0ccf6865e 100644 --- a/ruby/spec/jam_ruby/models/invited_user_spec.rb +++ b/ruby/spec/jam_ruby/models/invited_user_spec.rb @@ -18,6 +18,8 @@ describe InvitedUser do invited_user.sender.should_not be_nil invited_user.note.should be_nil invited_user.invited_by_administrator?.should be_false + #invited_user.sender.reload + invited_user.sender.first_invited_at.should_not be_nil end it 'create an invitation from admin-user' do diff --git a/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb b/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb new file mode 100644 index 000000000..c0acbc01b --- /dev/null +++ b/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' + +describe MusicSessionUserHistory do + + let(:some_user) { FactoryGirl.create(:user) } + let(:music_session) { FactoryGirl.create(:music_session) } + let(:history) { FactoryGirl.create(:music_session_history, :music_session => music_session) } + let(:user_history1) { FactoryGirl.create(:music_session_user_history, :history => history, :user => music_session.creator) } + let(:user_history2) { FactoryGirl.create(:music_session_user_history, :history => history, :user => some_user) } + + describe "create" do + it {user_history1.music_session_id.should == music_session.id } + it {user_history1.created_at.should_not be_nil } + it {user_history1.session_removed_at.should be_nil } + end + + describe "rating" do + + describe "success" do + + before(:each) do + user_history1.update_attribute(:rating ,0) + end + + it { user_history1.errors.any?.should be_false} + end + + describe "out of range" do + before(:each) do + user_history1.update_attribute(:rating, 3) + user_history1.save + end + + it { user_history1.errors.any?.should be_true} + end + + end + + describe "end_history" do + + it "histories created at the same time" do + user_history1.reload + user_history2.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 2 + end + + it "history2 ends before history1 starts" do + user_history2.created_at = user_history1.created_at - 2 + user_history2.session_removed_at = user_history1.created_at - 1 + user_history2.save.should be_true + user_history1.reload + user_history2.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 1 + end + + it "history2 starts after history1 starts" do + user_history2.created_at = user_history1.created_at + 1 + user_history2.session_removed_at = user_history1.created_at + 2 + user_history2.save.should be_true + user_history1.reload + user_history2.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 1 + end + + it "history2 is within bounds of history1 " do + user_history1.session_removed_at = user_history1.created_at + 3 + user_history2.created_at = user_history1.created_at + 1 + user_history2.session_removed_at = user_history1.created_at + 2 + user_history1.save.should be_true + user_history2.save.should be_true + user_history1.reload + user_history2.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 2 + end + + it "two histories with same user within bounds of history1" do + user_history3 = FactoryGirl.create(:music_session_user_history, :history => history, :user => some_user) + + # if user2 comes and goes 2 times while user one is there, it shouldn't be a false 3 + user_history1.session_removed_at = user_history1.created_at + 5 + user_history2.created_at = user_history1.created_at + 1 + user_history2.session_removed_at = user_history1.created_at + 2 + user_history3.created_at = user_history1.created_at + 3 + user_history3.session_removed_at = user_history1.created_at + 4 + user_history1.save.should be_true + user_history2.save.should be_true + user_history3.save.should be_true + user_history1.reload + user_history2.reload + user_history3.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 2 + end + + it "two histories with different user within bounds of history1" do + third_user = FactoryGirl.create(:user); + + user_history3 = FactoryGirl.create(:music_session_user_history, :history => history, :user => third_user) + + # if user2 comes and goes 2 times while user one is there, it shouldn't be a false 3 + user_history1.session_removed_at = user_history1.created_at + 5 + user_history2.created_at = user_history1.created_at + 1 + user_history2.session_removed_at = user_history1.created_at + 2 + user_history3.created_at = user_history1.created_at + 3 + user_history3.session_removed_at = user_history1.created_at + 4 + user_history1.save.should be_true + user_history2.save.should be_true + user_history3.save.should be_true + user_history1.reload + user_history2.reload + user_history3.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 2 + end + + it "two overlapping histories with different user within bounds of history1" do + third_user = FactoryGirl.create(:user); + + user_history3 = FactoryGirl.create(:music_session_user_history, :history => history, :user => third_user) + + # if user2 comes and goes 2 times while user one is there, it shouldn't be a false 3 + user_history1.session_removed_at = user_history1.created_at + 5 + user_history2.created_at = user_history1.created_at + 1 + user_history2.session_removed_at = user_history1.created_at + 3 + user_history3.created_at = user_history1.created_at + 2 + user_history3.session_removed_at = user_history1.created_at + 4 + user_history1.save.should be_true + user_history2.save.should be_true + user_history3.save.should be_true + user_history1.reload + user_history2.reload + user_history3.reload + user_history1.end_history + user_history1.max_concurrent_connections.should == 3 + end + + + end + +end + + diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index a0f7cd418..df1c01ef7 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -243,6 +243,23 @@ describe User do its(:remember_token) { should_not be_blank } end + describe "user progression only touches once" do + it "allows first touch" do + @user.update_progression_field (:first_downloaded_client_at) + @user.errors.any?.should be_false + @user.first_downloaded_client_at.should_not be_nil + end + + it "ignores second touch" do + time = DateTime.now - 1 + @user.update_progression_field(:first_downloaded_client_at, time) + first_value = @user.first_downloaded_client_at + @user.update_progression_field(:first_downloaded_client_at) + @user.errors.any?.should be_false + @user.first_downloaded_client_at.should == first_value + end + end + describe "authenticate (class-instance)" do before { @user.email_confirmed=true; @user.save } diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js index 700a44f5e..c5567d6eb 100644 --- a/web/app/assets/javascripts/ftue.js +++ b/web/app/assets/javascripts/ftue.js @@ -12,6 +12,7 @@ context.JK.FtueWizard = function(app) { context.JK.FtueWizard.latencyTimeout = true; context.JK.FtueWizard.latencyMS = Number.MAX_VALUE; + var rest = context.JK.Rest(); var logger = context.JK.logger; var jamClient = context.jamClient; @@ -110,10 +111,14 @@ logger.debug(latencyMS + " is <= 20. Setting FTUE status to true"); ftueSave(true); // Save the profile context.jamClient.FTUESetStatus(true); // No FTUE wizard next time + rest.userCertifiedGear({success:true}); // notify anyone curious about how it went $('div[layout-id=ftue]').trigger('ftue_success'); } + else { + rest.userCertifiedGear({success:false, reason:"latency=" + latencyMS}); + } updateGauge(); } diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index da68657ea..37f37ec4f 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -213,6 +213,44 @@ return deferred; } + function userDownloadedClient(options) { + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/users/progression/downloaded_client", + processData: false + }); + } + + function userCertifiedGear(options) { + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/users/progression/certified_gear", + processData: false, + data: JSON.stringify({ + success: options.success, + reason: options.reason + }) + }); + } + + function userSocialPromoted(options) { + var id = getId(options); + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/users/progression/social_promoted", + processData: false + }); + } + function signout() { return $.ajax({ type: "DELETE", @@ -245,6 +283,9 @@ this.serverHealthCheck = serverHealthCheck; this.acceptFriendRequest = acceptFriendRequest; this.signout = signout; + this.userDownloadedClient = userDownloadedClient; + this.userCertifiedGear = userCertifiedGear; + this.userSocialPromoted = userSocialPromoted; return this; }; diff --git a/web/app/assets/javascripts/landing/downloads.js b/web/app/assets/javascripts/landing/downloads.js index 0c45f2889..b06a8975f 100644 --- a/web/app/assets/javascripts/landing/downloads.js +++ b/web/app/assets/javascripts/landing/downloads.js @@ -43,6 +43,7 @@ var download = $(context._.template($('#client-download-link').html(), options, { variable: 'data' })); download.find('a').data('platform', platform).click(function() { + rest.userDownloadedClient(); context.JK.GA.trackDownload($(this).data('platform')); }); diff --git a/web/app/controllers/api_controller.rb b/web/app/controllers/api_controller.rb index b3ae388e4..2d1e54c5d 100644 --- a/web/app/controllers/api_controller.rb +++ b/web/app/controllers/api_controller.rb @@ -36,4 +36,13 @@ class ApiController < ApplicationController @user = User.find(params[:id]) end + + def optional_auth_user + if current_user.nil? + @user = nil + else + auth_user + end + + end end \ No newline at end of file diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index b0a6cb22b..59a2fd280 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -109,6 +109,19 @@ class ApiMusicSessionsController < ApiController respond_with @connection, responder: ApiResponder end + def participant_rating + @history = MusicSessionUserHistory.find(params[:id]) + @history.rating = params[:rating] + @history.save + + if @history.errors.any? + response.status = :unprocessable_entity + respond_with @history + else + render :json => {}, :status => :ok + end + end + def lookup_session @music_session = MusicSession.find(params[:id]) end diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 3b471097c..d55649882 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -11,7 +11,6 @@ class ApiUsersController < ApiController :notification_index, :notification_destroy, # notifications :band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy] - respond_to :json def index @@ -529,6 +528,48 @@ class ApiUsersController < ApiController end + + # user progression tracking + def downloaded_client + @user = current_user + @user.update_progression_field(:first_downloaded_client_at) + + if @user.errors.any? + respond_with @user, :status => :unprocessable_entity + return + end + render :json => {}, :status => 200 + + end + # user progression tracking + def qualified_gear + @user = current_user + if params[:success] + @user.update_progression_field(:first_certified_gear_at) + else + @user.failed_qualification(params[:reason]) + end + + if @user.errors.any? + respond_with @user, :status => :unprocessable_entity + return + end + + render :json => {}, :status => 200 + end + + # user progression tracking + def social_promoted + @user = current_user + @user.update_progression_field(:first_social_promoted_at) + + if @user.errors.any? + respond_with @user, :status => :unprocessable_entity + return + end + render :json => {}, :status => 200 + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) diff --git a/web/app/controllers/sessions_controller.rb b/web/app/controllers/sessions_controller.rb index c516b01d3..ca75e1e0e 100644 --- a/web/app/controllers/sessions_controller.rb +++ b/web/app/controllers/sessions_controller.rb @@ -13,6 +13,11 @@ class SessionsController < ApplicationController @login_error = true render 'new', :layout => "landing" else + + if jkclient_agent? + user.update_progression_field(:first_ran_client_at) + end + @session_only_cookie = !jkclient_agent? && !params[:user].nil? && 0 == params[:user][:remember_me].to_i complete_sign_in user end diff --git a/web/config/routes.rb b/web/config/routes.rb index f25a92662..0c48b0a2e 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -88,6 +88,8 @@ SampleApp::Application.routes.draw do match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail' match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_destroy', :via => :delete + match '/participant_histories/:id/rating' => 'api_music_sessions#participant_rating', :via => :post + # genres match '/genres' => 'api_genres#index', :via => :get @@ -174,6 +176,11 @@ SampleApp::Application.routes.draw do match '/users/:id/avatar' => 'api_users#delete_avatar', :via => :delete match '/users/:id/filepicker_policy' => 'api_users#generate_filepicker_policy', :via => :get + # user progression + match '/users/progression/downloaded_client' => 'api_users#downloaded_client', :via => :post + match '/users/progression/certified_gear' => 'api_users#qualified_gear', :via => :post + match '/users/progression/social_promoted' => 'api_users#social_promoted', :via => :post + # user recordings # match '/users/:id/recordings' => 'api_users#recording_index', :via => :get # match '/users/:id/recordings/:recording_id' => 'api_users#recording_show', :via => :get, :as => 'api_recording_detail' diff --git a/web/lib/music_session_manager.rb b/web/lib/music_session_manager.rb index c6c43b014..ad6386e4f 100644 --- a/web/lib/music_session_manager.rb +++ b/web/lib/music_session_manager.rb @@ -1,5 +1,6 @@ require 'recaptcha' -class MusicSessionManager < BaseManager +class +MusicSessionManager < BaseManager include Recaptcha::Verify diff --git a/web/spec/requests/music_sessions_api_spec.rb b/web/spec/requests/music_sessions_api_spec.rb index da97d050c..32746fddf 100755 --- a/web/spec/requests/music_sessions_api_spec.rb +++ b/web/spec/requests/music_sessions_api_spec.rb @@ -592,6 +592,20 @@ describe "Music Session API ", :type => :api do music_session_perf_data.music_session_user_history.should == MusicSessionUserHistory.find_by_client_id(client.client_id) end + it "rating" do + user = FactoryGirl.create(:user) + client = FactoryGirl.create(:connection, :user => user) + music_session = FactoryGirl.create(:music_session, :creator => user, :description => "My Session") + msuh = FactoryGirl.create(:music_session_user_history, :music_session_id => music_session.id, :client_id => client.client_id, :user_id => user.id) + msuh.rating.should be_nil + login(user) + post "/api/participant_histories/#{msuh.id}/rating.json", { :rating => 0 }.to_json, "CONTENT_TYPE" => "application/json" + last_response.status.should == 200 + msuh.reload + msuh.rating.should == 0 + + end + end diff --git a/web/spec/requests/user_progression_spec.rb b/web/spec/requests/user_progression_spec.rb new file mode 100644 index 000000000..cff28581d --- /dev/null +++ b/web/spec/requests/user_progression_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +# user progression is achieved by different aspects of the code working together in a cross-cutting fashion. +# due to this, it's nice to have a single place where all the parts of user progression are tested +# https://jamkazam.atlassian.net/wiki/pages/viewpage.action?pageId=3375145 + +describe "User Progression", :type => :api do + + include Rack::Test::Methods + + subject { page } + + def login(user) + post '/sessions', "session[email]" => user.email, "session[password]" => user.password + rack_mock_session.cookie_jar["remember_token"].should == user.remember_token + end + + describe "user progression" do + let(:user) { FactoryGirl.create(:user) } + let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} } + + before do + login(user) + end + + it "downloaded_client" do + user.first_downloaded_client_at.should be_nil + post "/api/users/progression/downloaded_client" + last_response.status.should eql(200) + JSON.parse(last_response.body).should eql({ }) + user.reload + user.first_downloaded_client_at.should_not be_nil + end + + it "downloaded_client twice" do + post "/api/users/progression/downloaded_client" + user.reload + first = user.first_downloaded_client_at + post "/api/users/progression/downloaded_client" + user.reload + user.first_downloaded_client_at.should == first + end + + it "qualified gear" do + user.first_certified_gear_at.should be_nil + post "/api/users/progression/certified_gear.json", { :success => true}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(200) + JSON.parse(last_response.body).should eql({ }) + user.reload + user.first_certified_gear_at.should_not be_nil + user.last_failed_certified_gear_at.should be_nil + user.last_failed_certified_gear_reason.should be_nil + end + + it "failed qualified gear" do + user.first_certified_gear_at.should be_nil + post "/api/users/progression/certified_gear.json", { :success => false, :reason => "latency=30"}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(200) + JSON.parse(last_response.body).should eql({ }) + user.reload + user.first_certified_gear_at.should be_nil + user.last_failed_certified_gear_at.should_not be_nil + user.last_failed_certified_gear_reason.should == "latency=30" + end + + + it "social promoted" do + user.first_social_promoted_at.should be_nil + post "/api/users/progression/social_promoted" + last_response.status.should eql(200) + JSON.parse(last_response.body).should eql({ }) + user.reload + user.first_social_promoted_at.should_not be_nil + end + + it "joined any session" do + user.first_music_session_at.should be_nil + client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1", :client_id => "1.1") + post '/api/sessions.json', defopts.merge({:client_id => client.client_id}).to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(201) + user.reload + user.first_music_session_at.should_not be_nil + end + + it "ran client" do + user.first_ran_client_at.should be_nil + # change user-agent to look like the real client, and see ran_client flag set + post '/sessions', {"session[email]" => user.email, "session[password]" => user.password}, { "HTTP_USER_AGENT" => " JamKazam " } + user.reload + user.first_ran_client_at.should_not be_nil + end + + it "real session" do + # to make a real session, we need a session at least 15 minutes long and containting 3 concurrent users + + user2 = FactoryGirl.create(:user) + user3 = FactoryGirl.create(:user) + + user.first_real_music_session_at.should be_nil + client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1", :client_id => "1_1") + client2 = FactoryGirl.create(:connection, :user => user2, :ip_address => "1.1.1.2", :client_id => "1_2") + client3 = FactoryGirl.create(:connection, :user => user3, :ip_address => "1.1.1.3", :client_id => "1_3") + post '/api/sessions.json', defopts.merge({:client_id => client.client_id}).to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(201) + music_session = JSON.parse(last_response.body) + + login(user2) + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(201) + + login(user3) + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(201) + + # instrument the created_at of the music_history field to be at the beginning of time, so that we cross the 15 minute threshold of a 'real session + history1 = MusicSessionUserHistory.where(:user_id => user.id, :music_session_id => music_session["id"]).first + history2 = MusicSessionUserHistory.where(:user_id => user2.id, :music_session_id => music_session["id"]).first + history3 = MusicSessionUserHistory.where(:user_id => user3.id, :music_session_id => music_session["id"]).first + history1.should_not be_nil + history2.should_not be_nil + history3.should_not be_nil + history1.created_at = Time.at(0) + history2.created_at = Time.at(0) + history3.created_at = Time.at(0) + history1.save + history2.save + history3.save + + # now log out of the session for all 3 users + login(user) + delete "/api/participants/#{client.client_id}.json", '', "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(204) + login(user2) + delete "/api/participants/#{client2.client_id}.json", '', "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(204) + login(user3) + delete "/api/participants/#{client3.client_id}.json", '', "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(204) + + user.reload + user2.reload + user3.reload + user.first_real_music_session_at.should_not be_nil + user2.first_real_music_session_at.should_not be_nil + user3.first_real_music_session_at.should_not be_nil + end + + it "invites user" do + user.first_invited_at.should be_nil + post '/api/invited_users.json', {:email => 'tester@jamkazam.com', :note => "please join"}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(201) + user.reload + user.first_invited_at.should_not be_nil + end + + it "good session" do + user = FactoryGirl.create(:user) + user.first_good_music_session_at.should be_nil + + client = FactoryGirl.create(:connection, :user => user) + music_session = FactoryGirl.create(:music_session, :creator => user, :description => "My Session") + msuh = FactoryGirl.create(:music_session_user_history, :music_session_id => music_session.id, :client_id => client.client_id, :user_id => user.id) + login(user) + post "/api/participant_histories/#{msuh.id}/rating.json", { :rating => 0 }.to_json, "CONTENT_TYPE" => "application/json" + + user.reload + user.first_good_music_session_at.should_not be_nil + end + end + +end