diff --git a/db/manifest b/db/manifest index 637e56cc4..30837d3ad 100755 --- a/db/manifest +++ b/db/manifest @@ -151,3 +151,4 @@ user_mods.sql connection_stale_expire.sql rename_chat_messages.sql fix_connection_fields.sql +session_ratings.sql diff --git a/db/up/session_ratings.sql b/db/up/session_ratings.sql new file mode 100644 index 000000000..68223a849 --- /dev/null +++ b/db/up/session_ratings.sql @@ -0,0 +1 @@ +ALTER TABLE music_sessions_user_history ADD COLUMN rating_comment TEXT; diff --git a/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb b/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb index 7b200eb3b..17f238090 100644 --- a/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb @@ -15,7 +15,7 @@ module JamRuby batch.did_send(emails) - mail(:to => emails, + mail(:to => emails, :from => batch.from_email, :subject => batch.subject) do |format| format.text diff --git a/ruby/lib/jam_ruby/models/email_batch.rb b/ruby/lib/jam_ruby/models/email_batch.rb index 74dff06ec..02c729dda 100644 --- a/ruby/lib/jam_ruby/models/email_batch.rb +++ b/ruby/lib/jam_ruby/models/email_batch.rb @@ -13,7 +13,7 @@ module JamRuby VAR_LAST_NAME = '@LASTNAME' DEFAULT_SENDER = "noreply@jamkazam.com" - BATCH_SIZE = 1000 + BATCH_SIZE = 5 BODY_TEMPLATE =< -1..1, :allow_nil => true belongs_to(:user, :class_name => "JamRuby::User", @@ -16,8 +17,13 @@ module JamRuby :class_name => "MusicSessionHistory", :foreign_key => "music_session_id") - validates_inclusion_of :rating, :in => 0..2, :allow_nil => true - after_save :track_user_progression + def self.latest_history(client_id) + self.where(:client_id => client_id) + .order('created_at DESC') + .limit(1) + .includes(:user) + .first + end def music_session_history @msh ||= JamRuby::MusicSessionHistory.find_by_music_session_id(self.music_session_id) @@ -104,10 +110,23 @@ module JamRuby self.perf_data.try(:uri) end - def track_user_progression - if self.rating == 0 - user.update_progression_field(:first_good_music_session_at) - end + def add_rating(rval, comment='') + rval = rval.to_i + self.rating = rval if 0 != rval + self.rating_comment = comment end + + MIN_SESSION_DURATION_RATING = 60 + + def should_rate_session? + (2 <= music_session_history.unique_users.all.count && + MIN_SESSION_DURATION_RATING < (Time.now - music_session_history.created_at).seconds) || + Rails.env.development? + end + + def good_rating? + 0 < self.rating.to_i + end + end end diff --git a/ruby/lib/jam_ruby/resque/google_analytics_event.rb b/ruby/lib/jam_ruby/resque/google_analytics_event.rb index 58c393084..05c24a1e8 100644 --- a/ruby/lib/jam_ruby/resque/google_analytics_event.rb +++ b/ruby/lib/jam_ruby/resque/google_analytics_event.rb @@ -25,7 +25,7 @@ module JamRuby 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) + return unless session_id && session = MusicSession.find_by_id(session_id) GoogleAnalyticsEvent.enqueue(CAT_SESS_DUR, ACTION_SESS_DUR, SESSION_INTERVALS[interval_idx]) interval_idx += 1 @@ -47,7 +47,7 @@ module JamRuby @queue = QUEUE_BAND_TRACKER def self.perform(session_id) - return unless session = MusicSession.find(session_id) + return unless session = MusicSession.find_by_id(session_id) band = session.band if band.in_real_session?(session) band.update_attribute(:did_real_session, true) 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 index 3d7713b86..be947eb67 100644 --- a/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb +++ b/ruby/spec/jam_ruby/models/music_sessions_user_history_spec.rb @@ -8,6 +8,7 @@ describe MusicSessionUserHistory do let(:user_history2) { FactoryGirl.create(:music_session_user_history, :history => music_session.music_session_history, :user => some_user) } describe "create" do + pending 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 } @@ -15,28 +16,35 @@ describe MusicSessionUserHistory do describe "rating" do - describe "success" do - - before(:each) do - user_history1.update_attribute(:rating ,0) - end - - it { user_history1.errors.any?.should be_false} + it "success" do + user_history1.update_attribute(:rating, 1) + expect( user_history1.errors.any? ).to eq(false) end - describe "out of range" do - before(:each) do - user_history1.update_attribute(:rating, 3) - user_history1.save - end + it "out of range" do + user_history1.rating = 2 + user_history1.save + expect( user_history1.errors.any? ).to eq(true) + end - it { user_history1.errors.any?.should be_true} + it 'should rate success' do + users = [user_history1, user_history2] + Timecop.travel(Time.now + (MusicSessionUserHistory::MIN_SESSION_DURATION_RATING * 1.5).seconds) + expect( user_history1.should_rate_session? ).to eq(true) + Timecop.return + end + + it 'should rate fails' do + users = [user_history1] + expect( user_history1.should_rate_session? ).to eq(false) + users = [user_history1, user_history2] + expect( user_history2.should_rate_session? ).to eq(false) end end describe "end_history" do - + pending it "histories created at the same time" do user_history1.reload user_history2.reload diff --git a/web/app/assets/images/content/icon_thumbsdown_big_off.png b/web/app/assets/images/content/icon_thumbsdown_big_off.png new file mode 100644 index 000000000..bd7e0261b Binary files /dev/null and b/web/app/assets/images/content/icon_thumbsdown_big_off.png differ diff --git a/web/app/assets/images/content/icon_thumbsdown_big_on.png b/web/app/assets/images/content/icon_thumbsdown_big_on.png new file mode 100644 index 000000000..69438d595 Binary files /dev/null and b/web/app/assets/images/content/icon_thumbsdown_big_on.png differ diff --git a/web/app/assets/images/content/icon_thumbsup_big_off.png b/web/app/assets/images/content/icon_thumbsup_big_off.png new file mode 100644 index 000000000..00ecd4f00 Binary files /dev/null and b/web/app/assets/images/content/icon_thumbsup_big_off.png differ diff --git a/web/app/assets/images/content/icon_thumbsup_big_on.png b/web/app/assets/images/content/icon_thumbsup_big_on.png new file mode 100644 index 000000000..39bcc1a8a Binary files /dev/null and b/web/app/assets/images/content/icon_thumbsup_big_on.png differ diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js index 11a96eed6..a2d30f72b 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -19,6 +19,11 @@ join : "Join" }; + var sessionQualityTypes = { + good : "Good", + poor : "Poor" + }; + var invitationTypes = { email : "Email", facebook : "Facebook", @@ -83,6 +88,7 @@ audioTest : "AudioTest", sessionCount : "SessionCount", sessionMusicians : "SessionMusicians", + sessionQuality : "SessionQuality", invite : "Invite", findSession : "FindSession", friendConnect : "Connect", @@ -174,6 +180,11 @@ context.ga('send', 'event', categories.sessionMusicians, joinOrCreate); } + function trackSessionQuality(goodOrPoor) { + assertOneOf(goodOrPoor, sessionQualityTypes); + context.ga('send', 'event', categories.sessionQuality, goodOrPoor); + } + function trackServiceInvitations(invitationType, numInvited) { assertOneOf(invitationType, invitationTypes); assertNumber(numInvited); @@ -271,6 +282,7 @@ var GA = {}; GA.Categories = categories; GA.SessionCreationTypes = sessionCreationTypes; + GA.SessionQualityTypes = sessionQualityTypes; GA.InvitationTypes = invitationTypes; GA.FriendConnectTypes = friendConnectTypes; GA.RecordingActions = recordingActions; @@ -281,6 +293,7 @@ GA.trackFTUECompletion = trackFTUECompletion; GA.trackSessionCount = trackSessionCount; GA.trackSessionMusicians = trackSessionMusicians; + GA.trackSessionQuality = trackSessionQuality; GA.trackServiceInvitations = trackServiceInvitations; GA.trackFindSessions = trackFindSessions; GA.virtualPageView = virtualPageView; diff --git a/web/app/assets/javascripts/rateSessionDialog.js b/web/app/assets/javascripts/rateSessionDialog.js new file mode 100644 index 000000000..644585bb5 --- /dev/null +++ b/web/app/assets/javascripts/rateSessionDialog.js @@ -0,0 +1,123 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.RateSessionDialog = function(app) { + var logger = context.JK.logger; + var dialogId = 'rate-session-dialog'; + var $scopeSelector = "[layout-id='rate-session-dialog']"; + var clientId = context.JK.JamServer.clientID; + + function reset() { + clientId = context.JK.JamServer.clientID; + $('#btn-rate-session-up', $scopeSelector).removeClass('selected'); + $('#btn-rate-session-down', $scopeSelector).removeClass('selected'); + $('#txt-rate-session-comment',"[layout-id='rate-session-dialog']").val(''); + } + + function showDialog() { + if (clientId) { + reset(); + $.ajax({ + type: "GET", + url: "/api/participant_histories/"+clientId + }).done(function (response) { + if (response && + response.hasOwnProperty('should_rate_session') && + true==response['should_rate_session']) { + app.layout.showDialog(dialogId); + } + }); + return true; + } + return false; + } + + function closeDialog() { + app.layout.closeDialog(dialogId); + } + + function getRating() { + if ($('#btn-rate-session-down', $scopeSelector).hasClass('selected')) { + return -1; + } else if ($('#btn-rate-session-up', $scopeSelector).hasClass('selected')) { + return 1; + } + return 0; + } + + function getComment() { + return $('#txt-rate-session-comment',"[layout-id='rate-session-dialog']").val(); + } + + function events() { + $('#btn-rate-session-cancel', $scopeSelector).click(function(evt) { + closeDialog(); + }); + $('#btn-rate-session-up', $scopeSelector).click(function(evt) { + if ($(this).hasClass('selected')) { + $(this).removeClass('selected') + } else { + $(this).addClass('selected'); + } + if ($('#btn-rate-session-down').hasClass('selected')) { + $('#btn-rate-session-down').removeClass('selected') + } + return false; + }); + $('#btn-rate-session-down', $scopeSelector).click(function(evt) { + if ($(this).hasClass('selected')) { + $(this).removeClass('selected') + } else { + $(this).addClass('selected'); + } + if ($('#btn-rate-session-up').hasClass('selected')) { + $('#btn-rate-session-up').removeClass('selected') + } + return false; + }); + $('#btn-rate-session-send', $scopeSelector).click(function(evt) { + var rr = getRating(), cc = getComment(); + if (0 == rr && 0 == cc.length) { + closeDialog(); + return false; + } + var url = "/api/participant_histories/"+clientId+"/rating"; + $.ajax({ + type: "POST", + url: url, + data: { rating: getRating(), comment: getComment() } + }).done(function (response) { + var qq = getRating(); + if (0 < qq) { + context.JK.GA.trackSessionQuality(context.JK.GA.SessionQualityTypes.good); + } else if (0 > qq){ + context.JK.GA.trackSessionQuality(context.JK.GA.SessionQualityTypes.poor); + } + closeDialog(); + }); + return false; + }); + } + + function beforeShow(data) { + // confirm user should see dialog + } + + function afterShow(data) { + } + + function initialize() { + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow + }; + app.bindDialog(dialogId, dialogBindings); + events(); + } + + this.initialize = initialize; + this.showDialog = showDialog; + }; +})(window,jQuery); diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index e667e27c0..8f9da6756 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -31,6 +31,7 @@ var playbackControls = null; var promptLeave = false; var backendMixerAlertThrottleTimer = null; + var rateSessionDialog = null; var rest = context.JK.Rest(); @@ -1305,15 +1306,27 @@ } } + function bailOut() { + promptLeave = false; + context.window.location = '/client#/home'; + } + function sessionLeave(evt) { evt.preventDefault(); - - promptLeave = false; - context.window.location = '/client#/home'; - + rateSession(); + bailOut(); return false; } + function rateSession() { + if (rateSessionDialog === null) { + rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); + rateSessionDialog.initialize(); + } + rateSessionDialog.showDialog(); + return true; + } + function sessionResync(evt) { evt.preventDefault(); var response = context.jamClient.SessionAudioResync(); diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index cd389c60f..387352b0d 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -727,4 +727,26 @@ table.vu td { #update-session-invite-musicians { margin: 10px; -} \ No newline at end of file +} + +.rate-thumbsup { + width:64px; + height:64px; + display:inline-block; + background-image:url('/assets/content/icon_thumbsup_big_off.png'); +} + +.rate-thumbsup.selected { + background-image:url('/assets/content/icon_thumbsup_big_on.png'); +} + +.rate-thumbsdown { + width:64px; + height:64px; + display:inline-block; + background-image:url('/assets/content/icon_thumbsdown_big_off.png'); +} + +.rate-thumbsdown.selected { + background-image:url('/assets/content/icon_thumbsdown_big_on.png'); +} diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index a7d96b3d1..d832b68a0 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -151,16 +151,27 @@ class ApiMusicSessionsController < ApiController end def participant_rating - @history = MusicSessionUserHistory.find(params[:id]) - @history.rating = params[:rating] - @history.save + if @history = MusicSessionUserHistory.latest_history(params[:client_id]) + if request.post? + @history.add_rating(params[:rating], params[:comment]) + @history.save - if @history.errors.any? - response.status = :unprocessable_entity - respond_with @history - else - render :json => {}, :status => :ok + if @history.errors.any? + response.status = :unprocessable_entity + respond_with @history + else + if @history.good_rating? && @history.user.first_good_music_session_at.nil? + @history.user.first_good_music_session_at = Time.now + @history.user.save + end + render :json => {}, :status => :ok + end + elsif request.get? + render :json => { :should_rate_session => @history.should_rate_session? }, :status => :ok end + else + render :json => { :message => ValidationMessages::SESSION_NOT_FOUND }, :status => 404 + end end def track_index diff --git a/web/app/views/clients/_rateSession.html.erb b/web/app/views/clients/_rateSession.html.erb new file mode 100644 index 000000000..fa87832f5 --- /dev/null +++ b/web/app/views/clients/_rateSession.html.erb @@ -0,0 +1,17 @@ +
+ +
+ <%= image_tag "shared/icon_session.png", {:height => 19, :width => 19, :class => "content-icon"} %> +

please rate your session

+
+
+
+       +

+ +

+ SEND FEEDBACK   NOT NOW, THANKS +
+
+ +
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index dd8d91baa..b2865f935 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -21,6 +21,7 @@ <%= render "clients/gear/gear_wizard" %> <%= render "terms" %> <%= render "leaveSessionWarning" %> +<%= render "rateSession" %> <%= render "alert" %> <%= render "sidebar" %> <%= render "createSession" %> diff --git a/web/config/routes.rb b/web/config/routes.rb index 6c6fd2a42..5e59b6c4d 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -167,7 +167,8 @@ SampleApp::Application.routes.draw do match '/sessions/:id/claimed_recording/:claimed_recording_id/start' => 'api_music_sessions#claimed_recording_start', :via => :post match '/sessions/:id/claimed_recording/:claimed_recording_id/stop' => 'api_music_sessions#claimed_recording_stop', :via => :post - match '/participant_histories/:id/rating' => 'api_music_sessions#participant_rating', :via => :post + match '/participant_histories/:client_id/rating' => 'api_music_sessions#participant_rating', :via => :post + match '/participant_histories/:client_id' => 'api_music_sessions#participant_rating', :via => :get # genres match '/genres' => 'api_genres#index', :via => :get diff --git a/web/spec/requests/music_sessions_api_spec.rb b/web/spec/requests/music_sessions_api_spec.rb index 592a375c9..c48cdd7e4 100755 --- a/web/spec/requests/music_sessions_api_spec.rb +++ b/web/spec/requests/music_sessions_api_spec.rb @@ -656,10 +656,10 @@ describe "Music Session API ", :type => :api do 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" + post "/api/participant_histories/#{msuh.client_id}/rating.json", { :rating => 1 }.to_json, "CONTENT_TYPE" => "application/json" last_response.status.should == 200 msuh.reload - msuh.rating.should == 0 + msuh.rating.to_i.should == 1 end it "track sync" do diff --git a/web/spec/requests/user_progression_spec.rb b/web/spec/requests/user_progression_spec.rb index c1f904f5c..42894e678 100644 --- a/web/spec/requests/user_progression_spec.rb +++ b/web/spec/requests/user_progression_spec.rb @@ -160,8 +160,9 @@ describe "User Progression", :type => :api do 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) + expect(msuh).to_not eq(nil) login(user) - post "/api/participant_histories/#{msuh.id}/rating.json", { :rating => 0 }.to_json, "CONTENT_TYPE" => "application/json" + post "/api/participant_histories/#{msuh.client_id}/rating.json", { :rating => 1 }.to_json, "CONTENT_TYPE" => "application/json" user.reload user.first_good_music_session_at.should_not be_nil