diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index fc3ad2a8a..cca5d6eda 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -843,5 +843,9 @@ module JamRuby stats end + + def lesson_session + music_session.lesson_session + end end end diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index 09db02a52..8dd085eb3 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -44,7 +44,6 @@ module JamRuby has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id" - validates :duration, presence: true, numericality: {only_integer: true} validates :lesson_booking, presence: true validates :lesson_type, inclusion: {in: LESSON_TYPES} @@ -73,7 +72,7 @@ module JamRuby scope :completed, -> { where(status: STATUS_COMPLETED) } scope :missed, -> { where(status: STATUS_MISSED) } scope :upcoming, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', Time.now) } - scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now ) } + scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now) } def create_charge if !is_test_drive? @@ -104,6 +103,7 @@ module JamRuby def music_session_id music_session.id end + def self.hourly_check analyse_sessions complete_sessions @@ -141,7 +141,7 @@ module JamRuby now = Time.zone.local_to_utc(now) half_hour_from_now = Time.zone.local_to_utc(half_hour_from_now) end - + MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where('sent_starting_notice = false').where('(scheduled_start > ? and scheduled_start < ?)', now, half_hour_from_now).each do |music_session| lession_session = music_session.lesson_session lession_session.send_starting_notice @@ -184,7 +184,7 @@ module JamRuby lesson_payment_charge.amount_in_cents / 100.0 end - def analysis_to_json(analysis) + def self.analysis_to_json(analysis, preserve_object = false) json = {} analysis.each do |k, v| @@ -202,7 +202,11 @@ module JamRuby json[k] = v end end - json.to_json + if preserve_object + json + else + json.to_json + end end def send_starting_notice @@ -212,6 +216,7 @@ module JamRuby self.sent_starting_notice = true self.save(validate: false) end + def session_completed LessonSession.transaction do self.lock! diff --git a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb index 11a7c77c8..d217ae4c4 100644 --- a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -43,7 +43,7 @@ module JamRuby # reason: 'both_fault' - def self.analyse(lesson_session) + def self.analyse(lesson_session, force = false) reason = nil teacher = nil student = nil @@ -69,7 +69,7 @@ module JamRuby # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest - if music_session.session_removed_at.nil? && !((music_session.scheduled_start + (lesson_session.duration * 60)) < Time.now) + if !force && (music_session.session_removed_at.nil? && !((music_session.scheduled_start + (lesson_session.duration * 60)) < Time.now)) reason = SESSION_ONGOING bill = false else diff --git a/web/Gemfile b/web/Gemfile index 8a5ce3885..df30e75e5 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -143,6 +143,7 @@ group :test, :cucumber do gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' gem 'capybara', '2.4.4' + gem 'rails-assets-sinon', source: 'https://rails-assets.org' #if ENV['JAMWEB_QT5'] == '1' # # necessary on platforms such as arch linux, where pacman -S qt5-webkit is your easiet option # gem "capybara-webkit", :git => 'git://github.com/thoughtbot/capybara-webkit.git' diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index bde47b613..817ab2dad 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -369,7 +369,9 @@ "JamTrackGroup": 15, "MetronomeGroup": 16, "MidiInputMusicGroup": 17, - "PeerMidiInputMusicGroup": 18 + "PeerMidiInputMusicGroup": 18, + "UsbInputMusicGroup": 19, + "PeerUsbInputMusicGroup": 20 }; context.JK.ChannelGroupLookup = { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index b3c03627c..4605d9eab 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2231,6 +2231,16 @@ }); } + function getLessonAnalysis(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: "/api/lesson_sessions/" + options.id + "/analysis", + dataType: "json", + contentType: 'application/json' + }); + } + function updateLessonSessionUnreadMessages(options) { return $.ajax({ @@ -2663,6 +2673,7 @@ this.submitStripe = submitStripe; this.getLessonSessions = getLessonSessions; this.getLesson = getLesson; + this.getLessonAnalysis = getLessonAnalysis; this.updateLessonSessionUnreadMessages = updateLessonSessionUnreadMessages; this.checkLessonCancel = checkLessonCancel; this.checkLessonReschedule = checkLessonReschedule; diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 7d83a8e56..c8f7b2fb0 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -20,6 +20,7 @@ //= require ./react-components/stores/VideoStore //= require ./react-components/stores/SessionStore //= require ./react-components/stores/SessionStatsStore +//= require ./react-components/stores/BroadcastStore //= require ./react-components/stores/ChatStore //= require ./react-components/stores/MixerStore //= require ./react-components/stores/ConfigureTracksStore diff --git a/web/app/assets/javascripts/react-components/BroadcastHolder.js.jsx.coffee b/web/app/assets/javascripts/react-components/BroadcastHolder.js.jsx.coffee index 10b4ac023..3232e3444 100644 --- a/web/app/assets/javascripts/react-components/BroadcastHolder.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BroadcastHolder.js.jsx.coffee @@ -11,8 +11,11 @@ BroadcastHolder = React.createClass( render: -> notification = [] if this.state.notification - notification.push(``) + if this.state.notification.isLesson + notification.push(``) + else + notification.push(``) `
diff --git a/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee b/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee new file mode 100644 index 000000000..9afff73ef --- /dev/null +++ b/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee @@ -0,0 +1,62 @@ +context = window + +@InLessonBroadcast = React.createClass({ + displayName: 'In Lesson Broadcast' + + displayTime: (minuteOffset = 0) -> + untilTime = @props.lessonSession.until + + timeString = '' + if untilTime.days != 0 + timeString += "#{untilTime.days} days, " + if untilTime.hours != 0 || timeString.length > 0 + timeString += "#{untilTime.hours} hours, " + if untilTime.minutes != 0 || timeString.length > 0 + timeString += "#{untilTime.minutes + minuteOffset} minutes, " + if untilTime.seconds != 0 || timeString.length > 0 + timeString += "#{untilTime.seconds} seconds" + + if timeString == '' + 'now!' + timeString + + render: () -> + console.log("@props.lessonSession", @props.lessonSession) + if @props.lessonSession.completed + if @props.lessonSession.success + content = `
+

This lesson is over.

+
` + else + content = `
+

This lesson is over, but will not be billed.

+
` + else if @props.lessonSession.beforeSession + content = `
+

This lesson will start in:

+

{this.displayTime()}

+
` + else if @props.lessonSession.initialWindow + content = `
+

You need to wait in this session for

+

{this.displayTime(10)}

+

to allow time for your teacher to join you. If you leave before this timer reaches zero, and your teacher joins this session, you will be marked absent and charged for the lesson.

+
` + else if @props.lessonSession.teacherFault + if @props.lessonSession.teacherPresent? + content = `
+

You may now leave the session. However, if you choose to stay in the session with the teacher, after 5 minutes together the session will be considered a success, and you will be billed.

+

If the two of you do not spend at least 5 minutes together in the session, your teacher will be marked absent and penalized for missing the lesson. You will not be charged for this lesson.

+
` + else + content = `
+

You may now leave the session.

+

Your teacher will be marked absent and penalized for missing the lesson. You will not be charged for this lesson.

+

We apologize for your inconvenience, and we will work to remedy this situation.

+
` + + `
+ {content} +
` + +}) diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee index b7d1daaf7..51f385a46 100644 --- a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -27,6 +27,16 @@ context = window found + findParticipantByUserId: (userId) -> + foundParticipant = null + for participant in @participants() + if participant.user.id == userId + foundParticipant = participant + break + + foundParticipant + + otherParticipants: () -> others = [] for participant in @participants() diff --git a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee index 9f3b5cfbf..14ff32687 100644 --- a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee @@ -11,13 +11,116 @@ BroadcastStore = Reflux.createStore( { listenables: broadcastActions + currentSession: null + currentLesson: null + broadcast: null + currentLessonTimer: null + teacherFault: false + init: -> + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.SessionStore, this.onSessionChange) + + onAppInit: (@app) -> + + + getTimeRemaining: (endtime) -> + t = Date.parse(endtime) - new Date().getTime() + seconds = Math.floor( (t/1000) % 60 ); + minutes = Math.floor( (t/1000/60) % 60 ); + hours = Math.floor( (t/(1000*60*60)) % 24 ); + days = Math.floor( t/(1000*60*60*24) ); + return { + 'total': t, + 'days': days, + 'hours': hours, + 'minutes': minutes, + 'seconds': seconds + }; + + lessonTick: () -> + @timeManagement() + @changed() + + timeManagement: () -> + lastCheck = $.extend({}, @currentLesson) + lessonSession = @currentLesson + lessonSession.until = @getTimeRemaining(lessonSession.scheduled_start) + if lessonSession.until.total < 0 + # we are past the start time + if lessonSession.until.total < 10 * 60 * 1000 # 10 minutes + lessonSession.initialWindow = false + else + lessonSession.initialWindow = true + lessonSession.beforeSession = false + else + # we are before the due time + lessonSession.initialWindow = false + lessonSession.beforeSession = true + + # if we've transitioned to a new window + + if !lessonSession.beforeSession && ((lastCheck.initialWindow || !lastCheck.initialWindow?) && !lessonSession.initialWindow) + logger.debug("BroadcastStore: lesson session 'initial window' transition") + rest.getLessonAnalysis({id: lessonSession.id}).done((response) => @lessonAnalysisDone(response)).fail(@app.ajaxError) + + lessonAnalysisDone: (@analysis) -> + + if !@currentLesson? + logger.debug("BroadcastStore: ignoring lessonAnalysisDone") + if @analysis.status == 'completed' + @currentLesson.completed = true + @currentLesson.success = @analysis.success + @changed() + else if @analysis.analysis.reason != 'teacher_fault' + @clearLesson() + else + @teacherFault = true + @changed() + + clearLesson: () -> + if @currentLesson? + @currentLesson = null + if @currentLessonTimer? + clearInterval(@currentLessonTimer) + @currentLessonTimer = null + @teacherFault = false + @changed() + + onSessionChange: (session) -> + + @session = session + currentSession = session.session + if currentSession? && currentSession.lesson_session? && session.inSession() + + @currentSession = currentSession + + lessonSession = currentSession.lesson_session + # so that receivers can check type of info coming at them via one-way events + lessonSession.isLesson = true + + if lessonSession.status == 'completed' + lessonSession.completed = true + lessonSession.success = lessonSession.success + #else + # rest.getLessonAnalysis({id: lessonSession.id}).done((response) => @lessonAnalysisDone(response)).fail(@app.ajaxError) + + @currentLesson = lessonSession + @timeManagement() + if !@currentLessonTimer? + @currentLessonTimer = setInterval((() => @lessonTick()), 1000) + @changed() + + else + @clearLesson() + onLoad: () -> logger.debug("loading broadcast notification...") onLoadCompleted: (response) -> if response.id? logger.debug("broadcast notification sync completed") - this.trigger(response) + @broadcast = response + @changed() onLoadFailed: (jqXHR) -> @@ -25,7 +128,16 @@ BroadcastStore = Reflux.createStore( logger.error("broadcast notification sync failed") onHide: () -> - this.trigger(null) + @broadcast = null + @changed() + + changed: () -> + if @currentLesson? + @currentLesson.teacherFault = @teacherFault + @currentLesson.teacherPresent = @session.findParticipantByUserId(@currentLesson.teacher_id) + this.trigger(@currentLesson) + else + this.trigger(@broadcast) } ) diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index ce18d8551..969aa2860 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -552,6 +552,15 @@ ConfigureTracksActions = @ConfigureTracksActions # TODO: find it via some REST API if not found? return $.Deferred().reject().promise(); + findParticipantByUserId: (userId) -> + foundParticipant = null + for participant in @participants() + if participant.user.id == userId + foundParticipant = participant + break + + foundParticipant + displayWhoCreatedRecording: (clientId) -> if @app.clientId != clientId # don't show to creator @findUserBy({clientId: clientId}) diff --git a/web/app/assets/stylesheets/client/musician.css.scss b/web/app/assets/stylesheets/client/musician.css.scss index 69f9b82fe..11c534e90 100644 --- a/web/app/assets/stylesheets/client/musician.css.scss +++ b/web/app/assets/stylesheets/client/musician.css.scss @@ -366,7 +366,7 @@ padding-top: 3px; } - + .easydropdown-wrapper { + .easydropdown { float:left; margin-left:2px; } diff --git a/web/app/assets/stylesheets/client/react-components/broadcast.css.scss b/web/app/assets/stylesheets/client/react-components/broadcast.css.scss index 25d43ad5f..a8a95677c 100644 --- a/web/app/assets/stylesheets/client/react-components/broadcast.css.scss +++ b/web/app/assets/stylesheets/client/react-components/broadcast.css.scss @@ -20,6 +20,17 @@ margin-left:60px; @include border_box_sizing; + + &.lesson { + p { + text-align:center; + color:$ColorTextTypical; + margin-bottom:10px; + } + .message { + width:100%; + } + } } .message { float:left; @@ -27,6 +38,12 @@ font-size:12px; } + .time { + font-size:20px; + color:white; + font-weight:bold; + } + .actions { float:right; text-align: right; diff --git a/web/app/controllers/api_lesson_sessions_controller.rb b/web/app/controllers/api_lesson_sessions_controller.rb index 96a73b9d3..f09d6d1a0 100644 --- a/web/app/controllers/api_lesson_sessions_controller.rb +++ b/web/app/controllers/api_lesson_sessions_controller.rb @@ -20,9 +20,25 @@ class ApiLessonSessionsController < ApiController end + def analysis + if @lesson_session.analysed + data = JSON.parse(@lesson_session.analysis) + else + data = LessonSession.analysis_to_json(LessonSessionAnalyser.analyse(@lesson_session), true) + end + + response = { + analysis: data, + status: @lesson_session.status, + success: @lesson_session.success + } + + render :json => response, :status => 200 + end + def start_time if !current_user.admin - response = { message: 'not admin' } + response = {message: 'not admin'} render :json => response, :status => 422 else time = Time.zone.local_to_utc(Time.now + params[:minutes].to_i * 60) @@ -51,22 +67,22 @@ class ApiLessonSessionsController < ApiController def reschedule_check - # check if within 24 hours + # check if within 24 hours - if params[:update_all] - # check if the next scheduled lesson is doable - if 24.hours.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start - response = { message: 'time_limit' } - render :json => response, :status => 422 - return - end - else - if 24.hours.from_now > @lesson_session.music_session.scheduled_start - response = { message: 'time_limit' } - render :json => response, :status => 422 - return - end + if params[:update_all] + # check if the next scheduled lesson is doable + if 24.hours.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start + response = {message: 'time_limit'} + render :json => response, :status => 422 + return end + else + if 24.hours.from_now > @lesson_session.music_session.scheduled_start + response = {message: 'time_limit'} + render :json => response, :status => 422 + return + end + end render :json => {}, :status => 200 end @@ -78,13 +94,13 @@ class ApiLessonSessionsController < ApiController if params[:update_all] # check if the next scheduled lesson is doable if 24.hours.from_now > @lesson_session.booking.next_lesson.music_session.scheduled_start - response = { message: 'time_limit' } + response = {message: 'time_limit'} render :json => response, :status => 422 return end else if 24.hours.from_now > @lesson_session.music_session.scheduled_start - response = { message: 'time_limit' } + response = {message: 'time_limit'} render :json => response, :status => 422 return end @@ -112,4 +128,5 @@ class ApiLessonSessionsController < ApiController raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end end + end diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 61067a716..83da8aa32 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -74,6 +74,10 @@ else attributes :id, :sender_id, :receiver_id } + child(lesson_session: :lesson_session) { + attributes :id, :scheduled_start, :status, :teacher_id, :success, :duration + } + # only show join_requests if the current_user is in the session child({:join_requests => :join_requests}, :if => lambda { |music_session| music_session.users.exists?(current_user) } ) { attributes :id, :text diff --git a/web/config/routes.rb b/web/config/routes.rb index da7300efb..9d5c3ac0b 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -696,6 +696,7 @@ SampleApp::Application.routes.draw do match '/lesson_sessions/:id/start_time' => 'api_lesson_sessions#start_time', :via => :post match '/lesson_sessions/:id/reschedule_check' => 'api_lesson_sessions#reschedule_check', :via => :post match '/lesson_sessions/:id/cancel_check' => 'api_lesson_sessions#cancel_check', :via => :post + match '/lesson_sessions/:id/analysis' => 'api_lesson_sessions#analysis', :via => :get match '/lesson_bookings' => 'api_lesson_bookings#create', :via => :post match '/lesson_bookings/:id/accept' => 'api_lesson_bookings#accept', :via => :post match '/lesson_bookings/:id/counter' => 'api_lesson_bookings#counter', :via => :post diff --git a/web/spec/features/lesson_session_broadcast_spec.rb b/web/spec/features/lesson_session_broadcast_spec.rb new file mode 100644 index 000000000..f58363c0a --- /dev/null +++ b/web/spec/features/lesson_session_broadcast_spec.rb @@ -0,0 +1,19 @@ +# verifies the + +describe "Lesson Session Broadcast", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user) } + let(:ams) { FactoryGirl.create(:active_music_session, creator: user) } + + before(:each) do + ActiveMusicSession.delete_all + MusicSession.delete_all + end + + it "shows before message" do + testdrive_lesson + end +end +