From 78d46c7e7c71b0e2eec5c3a1172ce72fb0bec94b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 1 Apr 2016 08:12:24 -0500 Subject: [PATCH] * decent 1st cut of lesson-booking UI done --- admin/app/admin/lesson_session.rb | 4 +- db/up/lessons.sql | 1 + pb/src/client_container.proto | 2 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 4 +- .../student_lesson_canceled.html.erb | 2 +- ruby/lib/jam_ruby/message_factory.rb | 10 +- ruby/lib/jam_ruby/models/lesson_booking.rb | 30 ++-- .../jam_ruby/models/lesson_booking_slot.rb | 1 + ruby/lib/jam_ruby/models/lesson_session.rb | 98 +++++++++---- ruby/lib/jam_ruby/models/notification.rb | 20 ++- .../assets/javascripts/AAB_message_factory.js | 1 + .../assets/javascripts/notificationPanel.js | 21 ++- .../LessonBooking.js.jsx.coffee | 129 +++++++++++++----- .../LessonBookingDecision.js.jsx.coffee | 45 ++++-- .../LessonBookingScreen.css.scss | 10 +- .../api_lesson_bookings_controller.rb | 39 ++++-- web/app/views/api_lesson_bookings/show.rabl | 10 +- .../views/api_users/notification_index.rabl | 2 +- web/lib/tasks/lesson.rake | 43 +++++- 19 files changed, 343 insertions(+), 129 deletions(-) diff --git a/admin/app/admin/lesson_session.rb b/admin/app/admin/lesson_session.rb index a75b00fba..c58eb4658 100644 --- a/admin/app/admin/lesson_session.rb +++ b/admin/app/admin/lesson_session.rb @@ -17,8 +17,8 @@ ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do scope("Completed" ) { |scope| scope.unscoped.completed.order('created_at desc') } index do - column "User Link" do |lesson_sesson| - lesson_booking = lesson_sesson.lesson_booking + column "User Link" do |lesson_session| + lesson_booking = lesson_session.lesson_booking span do link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" end diff --git a/db/up/lessons.sql b/db/up/lessons.sql index 462da8bed..4711f41fb 100644 --- a/db/up/lessons.sql +++ b/db/up/lessons.sql @@ -135,6 +135,7 @@ CREATE TABLE lesson_booking_slots ( ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); ALTER TABLE lesson_bookings ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id); diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 9f8fcb39d..57799fc7b 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -695,6 +695,7 @@ message LessonMessage { optional bool student_directed = 8; optional string purpose = 9; optional string sender_name = 10; + optional string lesson_session_id = 11; } message ScheduledJamclassInvitation { @@ -705,6 +706,7 @@ message ScheduledJamclassInvitation { optional string session_date = 5; optional string notification_id = 6; optional string created_at = 7; + optional string lesson_session_id = 8; } diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 07e04e52d..0af25d8fb 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -845,7 +845,7 @@ module JamRuby mail(:to => email, :subject => subject) do |format| format.text - format.html + format.html { render :layout => "from_user_mailer" } end end @@ -870,7 +870,7 @@ module JamRuby mail(:to => email, :subject => subject) do |format| format.text - format.html + format.html { render :layout => "from_user_mailer" } end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb index 26194e95f..484bccb78 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb @@ -1,4 +1,4 @@ -<% provide(:title, @subject) %> +<% provide(:title, @subject) %> <% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> <% content_for :note do %> diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index f329b8fcf..30763c028 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -524,7 +524,7 @@ module JamRuby ) end - def scheduled_jamclass_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) + def scheduled_jamclass_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at, lesson_session_id) scheduled_jamclas_invitation = Jampb::ScheduledJamclassInvitation.new( :session_id => session_id, :photo_url => photo_url, @@ -532,7 +532,8 @@ module JamRuby :session_name => session_name, :session_date => session_date, :notification_id => notification_id, - :created_at => created_at + :created_at => created_at, + lesson_session_id: lesson_session_id ) Jampb::ClientMessage.new( @@ -969,7 +970,7 @@ module JamRuby end # creates the general purpose text message - def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose) + def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose, lesson_session_id) lesson_message = Jampb::LessonMessage.new( :photo_url => sender_photo_url, :sender_name => sender_name, @@ -980,7 +981,8 @@ module JamRuby :music_session_id => music_session_id, :created_at => created_at, :student_directed => student_directed, - :purpose => purpose + :purpose => purpose, + :lesson_session_id => lesson_session_id ) Jampb::ClientMessage.new( diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index c8e67efb9..a2feaefac 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -131,7 +131,8 @@ module JamRuby def next_lesson if recurring - lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first + session = lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first + LessonSession.find(session.id) if session else lesson_sessions[0] end @@ -148,10 +149,12 @@ module JamRuby self.default_slot = slot self.accepter = accepter - if !self.save + success = self.save + + if !success puts "unable to accept lesson booking #{errors.inspect}" - raise ActiveRecord::Rollback end + success end def counter(lesson_session, proposer, slot) @@ -159,9 +162,7 @@ module JamRuby self.lesson_booking_slots << slot self.counter_slot = slot #self.status = STATUS_COUNTERED - if !self.save - raise ActiveRecord::Rollback - end + self.save end def automatically_default_slot @@ -301,7 +302,7 @@ module JamRuby music_session = MusicSession.create(student, { name: "#{display_type2} JamClass taught by #{teacher.name}", - description: "This is a #{lesson_length}-minute #{display_type2} with #{teacher.name}.", + description: "This is a #{lesson_length}-minute #{display_type2} lesson with #{teacher.name}.", musician_access: false, fan_access: false, genres: ['other'], @@ -377,7 +378,8 @@ module JamRuby end def validate_accepted - if self.status_was != STATUS_REQUESTED && self.status_was != STATUS_COUNTERED + # accept is multipe purpose; either accept the initial request, or a counter slot + if self.status_was != STATUS_REQUESTED && counter_slot.nil? # && self.status_was != STATUS_COUNTERED self.errors.add(:status, "This lesson is already #{self.status}.") end @@ -387,7 +389,7 @@ module JamRuby def send_notices UserMailer.student_lesson_request(self).deliver UserMailer.teacher_lesson_request(self).deliver - Notification.send_lesson_message('accept', lesson_sessions[0], false) # TODO: this isn't quite an 'accept' + Notification.send_lesson_message('requested', lesson_sessions[0], false) # TODO: this isn't quite an 'accept' self.sent_notices = true self.save end @@ -408,7 +410,7 @@ module JamRuby elsif is_test_drive? "TestDrive" elsif is_normal? - "Purchased Lesson" + "Single" end end @@ -481,6 +483,9 @@ module JamRuby am_pm = 'pm' else hour = slot.hour + if hour == 0 + hour = 12 + end am_pm = 'am' end @@ -497,7 +502,8 @@ module JamRuby self.status = STATUS_CANCELED self.cancel_message = message self.canceler = canceler - if save + success = save + if success if approved_before? # just tell both people it's cancelled, to act as confirmation Notification.send_lesson_message('canceled', next_lesson, false) @@ -523,6 +529,8 @@ module JamRuby msg = ChatMessage.create(canceler, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, other, self) end + + success end def card_approved diff --git a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb index 2f45c55a8..1b0afeb54 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb @@ -9,6 +9,7 @@ module JamRuby belongs_to :proposer, class_name: "JamRuby::User" has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot has_one :countered_booking, class_name: "JamRuby::LessonBooking", foreign_key: :counter_slot_id, inverse_of: :counter_slot + has_one :countered_lesson, class_name: "JamRuby::LessonSession", foreign_key: :counter_slot_id, inverse_of: :counter_slot SLOT_TYPE_SINGLE = 'single' SLOT_TYPE_RECURRING = 'recurring' diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index 4c861f24b..1452fae8e 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -35,9 +35,11 @@ module JamRuby belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + validates :duration, presence: true, numericality: {only_integer: true} validates :lesson_booking, presence: true validates :lesson_type, inclusion: {in: LESSON_TYPES} @@ -59,12 +61,12 @@ module JamRuby after_save :manage_slot_changes after_create :create_charge - scope :approved, -> { where(status: STATUS_APPROVED) } - scope :requested, -> { where(status: STATUS_REQUESTED) } - scope :canceled, -> { where(status: STATUS_CANCELED) } - scope :suspended, -> { where(status: STATUS_SUSPENDED) } - scope :completed, -> { where(status: STATUS_COMPLETED) } - scope :missed, -> { where(status: STATUS_MISSED) } + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :completed, -> { where(status: STATUS_COMPLETED) } + scope :missed, -> { where(status: STATUS_MISSED) } def create_charge if !is_test_drive? @@ -357,8 +359,8 @@ module JamRuby end # check 24 hour window - if Time.now.to_i - scheduled_start.to_i > 24 * 60 * 60 - self.errors.add(:base, "This session is due to start within 24 hours and can not be canceled") + if scheduled_start.to_i - Time.now.to_i < 24 * 60 * 60 + self.errors.add(:base, "This session is due to start within 24 hours and can not be canceled.") end self.canceling = false @@ -449,6 +451,7 @@ module JamRuby # teacher accepts the lesson def accept(params) + response = self LessonSession.transaction do message = params[:message] @@ -469,7 +472,10 @@ module JamRuby if self.save # also let the lesson_booking know we got accepted - lesson_booking.accept(self, slot, accepter) + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + end UserMailer.student_lesson_accepted(self, message, slot).deliver UserMailer.teacher_lesson_accepted(self, message, slot).deliver chat_message = message ? "Lesson Approved: '#{message}'" : "Lesson Approved" @@ -481,14 +487,19 @@ module JamRuby else @@log.error("unable to accept slot #{slot.id} for lesson #{self.id}") puts("unable to accept slot #{slot.id} for lesson #{self.id}") + response = self + raise ActiveRecord::Rollback end else # this implies a new slot has been countered, and now approved if self.save if slot.update_all - lesson_booking.accept(self, slot, accepter) - chat_message = message ? "All Lesson Times Updated: '#{message}'" : "All Lesson Times Updated" + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + end + chat_message = message ? "All Lesson Times Updated: '#{message}'" : "All Lesson Times Updated" msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept' UserMailer.student_lesson_update_all(self, message, slot).deliver @@ -510,39 +521,57 @@ module JamRuby else @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") puts("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + response = self + raise ActiveRecord::Rollback end end end + response end def counter(params) - proposer = params[:proposer] - slot = params[:slot] - message = params[:message] + response = self + LessonSession.transaction do + proposer = params[:proposer] + slot = params[:slot] + message = params[:message] - self.countering = true - slot.proposer = proposer - slot.lesson_session = self - slot.message = message - self.lesson_booking_slots << slot - self.countered_slot = slot - self.countered_lesson = self - self.status = STATUS_COUNTERED - if self.save - if slot.update_all || lesson_booking.is_requested? - lesson_booking.counter(self, proposer, slot) + update_all = slot.update_all || !lesson_booking.recurring + self.countering = true + slot.proposer = proposer + slot.lesson_session = self + slot.message = message + self.lesson_booking_slots << slot + self.countered_slot = slot + self.countered_lesson = self + self.status = STATUS_COUNTERED + if !update_all + self.counter_slot = slot end - else - raise ActiveRecord::Rollback + if self.save + if update_all + if !lesson_booking.counter(self, proposer, slot) + response = lesson_booking + raise ActiveRecord::Rollback + end + end + else + response = self + raise ActiveRecord::Rollback + end + + + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + Notification.send_lesson_message('counter', self, slot.is_teacher_created?) end - msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) - Notification.send_lesson_message('counter', self, slot.is_teacher_created?) + response end # teacher accepts the lesson def cancel(params) + response = self LessonSession.transaction do canceler = params[:canceler] @@ -563,7 +592,10 @@ module JamRuby if self.save if update_all - lesson_booking.cancel(canceler, other, message) + if !lesson_booking.cancel(canceler, other, message) + response = lesson_booking + raise ActiveRecord::Rollback + end else msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, lesson_booking) Notification.send_lesson_message('canceled', self, false) @@ -571,8 +603,14 @@ module JamRuby UserMailer.student_lesson_canceled(self, message).deliver UserMailer.teacher_lesson_canceled(self, message).deliver end + else + response = self + raise ActiveRecord::Rollback end + end + + response end def description(lesson_booking) diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 275fd069d..d47ecff00 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -12,6 +12,7 @@ module JamRuby belongs_to :source_user, :class_name => "JamRuby::User", :foreign_key => "source_user_id" belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id" belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id" + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession", :foreign_key => "lesson_session_id" belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id" belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id" @@ -62,7 +63,7 @@ module JamRuby end end - self.class.format_msg(self.description, {:user => source_user, target: target_user :band => band, :session => session, purpose: purpose, student_directed: student_directed}) + self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed}) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -275,7 +276,6 @@ module JamRuby when NotificationTypes::SCHEDULED_JAMCLASS_INVITATION if student_directed - "You have been scheduled to take a JamClass with #{user.name}." else "You have been scheduled to teach a JamClass to #{user.name}" @@ -420,6 +420,7 @@ module JamRuby notification.purpose = purpose notification.session_id = lesson_session.music_session.id + notification.lesson_session_id = lesson_session.id notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose}) @@ -438,7 +439,8 @@ module JamRuby notification.session_id, notification.created_date, notification.student_directed, - notification.purpose + notification.purpose, + notification.lesson_session_id ) @@mq_router.publish_to_user(notification.target_user.id, message) @@ -730,6 +732,8 @@ module JamRuby notification.source_user_id = source_user.id notification.target_user_id = target_user.id notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = false #notification.message = notification_msg notification.save @@ -744,7 +748,8 @@ module JamRuby music_session.name, music_session.pretty_scheduled_start(false), notification.id, - notification.created_date + notification.created_date, + notification.lesson_session.id ) @@mq_router.publish_to_user(target_user.id, msg) @@ -762,13 +767,15 @@ module JamRuby student = target_user = user teacher = source_user = music_session.lesson_session.teacher - notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {teacher: teacher, student_directed: true}) + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: teacher, student_directed: true}) notification = Notification.new notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION notification.source_user_id = source_user.id notification.target_user_id = target_user.id notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = true #notification.message = notification_msg notification.save @@ -781,7 +788,8 @@ module JamRuby music_session.name, music_session.pretty_scheduled_start(false), notification.id, - notification.created_date + notification.created_date, + notification.lesson_session_id ) @@mq_router.publish_to_user(target_user.id, msg) diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 5483aa57f..f75d62be4 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -52,6 +52,7 @@ SCHEDULED_SESSION_COMMENT : "SCHEDULED_SESSION_COMMENT", SCHEDULED_JAMCLASS_INVITATION : "SCHEDULED_JAMCLASS_INVITATION", + LESSON_MESSAGE : "LESSON_MESSAGE", // recording notifications MUSICIAN_RECORDING_SAVED : "MUSICIAN_RECORDING_SAVED", diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index fd651e60d..fcf3b4474 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -440,8 +440,11 @@ else if (type === context.JK.MessageType.SCHEDULED_SESSION_INVITATION) { linkSessionInfoNotification(payload, $notification, $btnNotificationAction); } + else if (type === context.JK.MessageType.LESSON_MESSAGE) { + linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + } else if (type === context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION) { - linkSessionInfoNotification(payload, $notification, $btnNotificationAction); + linkLessonInfoNotification(payload, $notification, $btnNotificationAction); } else if (type === context.JK.MessageType.SCHEDULED_SESSION_RSVP) { var $action_btn = $notification.find($btnNotificationAction); @@ -483,6 +486,14 @@ }); } + function linkLessonInfoNotification(payload, $notification, $btnNotificationAction) { + var $action_btn = $notification.find($btnNotificationAction); + $action_btn.text('LESSON DETAILS'); + $action_btn.click(function() { + gotoLessonBookingPage({"lesson_session_id": payload.lesson_session_id}); + }); + } + function acceptBandInvitation(args) { rest.updateBandInvitation( args.band_id, @@ -926,9 +937,9 @@ text: "MORE INFO", rel: "external", "class": "button-orange", - callback: openSessionInfoWebPage, + callback: gotoLessonBookingPage, callback_args: { - "session_id": payload.session_id + "lesson_session_id": payload.lesson_session_id } }] ); @@ -1365,6 +1376,10 @@ context.JK.popExternalLink('/sessions/' + args.session_id + '/details'); } + function gotoLessonBookingPage(args) { + window.location.href = "/client#/jamclass/lesson-booking/" + args.lesson_session_id + } + function deleteNotificationHandler(evt) { evt.stopPropagation(); var notificationId = $(this).attr('notification-id'); diff --git a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee index e4ab134e5..893f1ef44 100644 --- a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee @@ -46,8 +46,6 @@ UserStore = context.UserStore slot.creatorRoleRelative = "your" slot.mySlot = @mySlot(slot) - console.log("SLOT", slot) - componentWillUpdate: (nextProps, nextState) -> if nextState.booking? booking = nextState.booking @@ -72,19 +70,24 @@ UserStore = context.UserStore beforeShow: (e) -> afterShow: (e) -> - @setState({updating: true}) + @setState({updating: true, counterErrors: null, cancelErrors: null}) rest.getLessonBooking({ id: e.id, }).done((response) => @getLessonBookingDone(response)).fail(@app.ajaxError) - getLessonBookingDone: (response) -> - - if response.counter_slot? - startSlotDecision = response.counter_slot.id + updateBookingState: (booking) -> + if booking.counter_slot? + startSlotDecision = booking.counter_slot.id else - startSlotDecision = response.default_slot.id + if booking.accepter_id? + startSlotDecision = 'counter' + else + startSlotDecision = booking.default_slot.id - @setState({booking: response, updating: false, slot_decision: startSlotDecision}) + @setState({booking: booking, updating: false, slot_decision: startSlotDecision, updatingLesson: false}) + + getLessonBookingDone: (response) -> + @updateBookingState(response) toJamClassMain: (e) -> e.preventDefault() @@ -96,58 +99,82 @@ UserStore = context.UserStore onAccept: () -> - @setState({updatingLesson: true}) + @setState({updatingLesson: true, counterErrors: null, cancelErrors: null}) if @state.slot_decision == 'counter' request = @getSlotData(0) request.id = this.state.booking.id request.timezone = window.jstz.determine().name() request.message = @getMessage() + request.update_all = true rest.counterLessonBooking(request).done((response) => @counterLessonBookingDone(response)).fail((response) => @counterLessonBookingFail(response)) else if @state.slot_decision == 'decline' request = {} request.message = @getMessage() request.id = this.state.booking.id + request.update_all = true rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response)).fail((response) => @cancelLessonBookingFail(response)) - else + else if @state.slot_decision request = {} request.message = @getMessage() request.id = this.state.booking.id request.slot = this.state.slot_decision rest.acceptLessonBooking(request).done((response) => @acceptLessonBookingDone(response)).fail((response) => @acceptLessonBookingFail(response)) - onCancelLesson: (e) -> - @setState({updatingLesson: true}) - request = {} - request.message = @getMessage() - request.id = this.state.booking.id - rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response)).fail(@app.ajaxError) + # {"errors":{"lesson_booking_slots":["is invalid"]},"_children":{"lesson_booking_slots":[{"errors":{}},{"errors":{}},{"errors":{"day_of_week":["must be specified"]}}]}} + dayOfWeekMissing: (errors) -> + console.log("errors", errors) + childErrors = errors._children + if childErrors + for key, errorData of childErrors + for slotErrors in errorData + if slotErrors.errors?.day_of_week? + return true + return false acceptLessonBookingDone: (response ) -> logger.debug("accept lesson booking done") - @setState({booking:response, updatingLesson: false}) + @updateBookingState(response) cancelLessonBookingDone: (response ) -> logger.debug("cancel lesson booking done") - @setState({booking:response, updatingLesson: false}) + @updateBookingState(response) counterLessonBookingDone: (response ) -> logger.debug("counter lesson booking done") - @setState({booking:response, updatingLesson: false}) + @updateBookingState(response) - counterLessonBookingFail: (response ) -> + counterLessonBookingFail: (jqXHR ) -> @setState({updatingLesson: false}) - logger.debug("counter lesson booking failed", response) - @app.ajaxError(arguments[0], arguments[1], arguments[2]) + logger.debug("counter lesson booking failed") + + handled = false + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + + if @dayOfWeekMissing(errors) + handled = true + @setState({counterErrors: {errors: {day_of_week: ["must be specified"]}}}) + + if !handled + @app.ajaxError(arguments[0], arguments[1], arguments[2]) acceptLessonBookingFail: (response ) -> @setState({updatingLesson: false}) logger.debug("accept lesson booking failed", response) @app.ajaxError(arguments[0], arguments[1], arguments[2]) - cancelLessonBookingFail: (response ) -> + cancelLessonBookingFail: (jqXHR) -> @setState({updatingLesson: false}) - logger.debug("cancel lesson booking failed", response) - @app.ajaxError(arguments[0], arguments[1], arguments[2]) + logger.debug("cancel lesson booking failed", jqXHR) + handled = false + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + if errors.errors?.base? + handled = true + window.JK.Banner.showAlert("Unable to Cancel Lesson", errors.errors?.base[0]) + if !handled + @app.ajaxError(arguments[0], arguments[1], arguments[2]) getMessage: () -> @root.find('textarea.message').val() @@ -364,13 +391,14 @@ UserStore = context.UserStore hour = slot.hour - 12 if hour == 0 hour = 12 - end am_pm = 'pm' else hour = slot.hour + if hour == 0 + hour = 12 am_pm = 'am' - "#{hour + 1}:#{slot.minute}#{am_pm}" + "#{context.JK.padString(hour.toString(), 2)}:#{context.JK.padString(slot.minute.toString(), 2)}#{am_pm} (#{slot.timezone})" isNow: () -> startTime = new Date(@state.booking.next_lesson.scheduled_start).getTime() @@ -392,6 +420,20 @@ UserStore = context.UserStore {this.userHeader(this.other())} {this.nextLessonSummary()} ` + + nextLessonSummaryRow: () -> + if @isActive() + if @isNow() + data =`

You should join this session immediately: {this.sessionLink()}

` + else if @isPast() + data =`

This lesson is over.

` + else + data = `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + else if @isRequested() + data = `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + `
+ {data} +
` nextLessonSummary: () -> if @isActive() if @isNow() @@ -434,6 +476,23 @@ UserStore = context.UserStore ` + decisionProps: (slots) -> + { + onSlotDecision: this.onSlotDecision, + initial: this.neverAccepted(), + counter: this.isCounter(), + is_recurring: this.isRecurring(), + slot_decision: this.state.slot_decision, + slots: slots, + otherRole: this.otherRole(), + onUserDecision: this.onAccept, + onUserCancel: this.onCancel, + disabled: this.state.updatingLesson, + selfLastToAct: this.selfLastToAct(), + counterErrors: this.state.counterErrors, + cancelErrors: this.state.cancelErrors + } + render: () -> if @state.updating @renderLoading() @@ -575,9 +634,7 @@ UserStore = context.UserStore

You can do this manually today; we will soon add a easier way to do so automatically.

{this.nextLessonSummary()} -
- {this.renderCancelLesson()} -
+ ` renderStudentCanceled: () -> @@ -590,6 +647,7 @@ UserStore = context.UserStore renderStudentCountered: () -> @renderCountered() + renderTeacherRequested: () -> if @isTestDrive() @@ -603,15 +661,13 @@ UserStore = context.UserStore {action} {this.slotMessage(this.state.booking.default_slot)} - + ` renderTeacherApproved: () -> `
{this.nextLessonSummaryWithAvatar()} -
- {this.renderCancelLesson()} -
+
` renderTeacherCanceled: () -> @@ -664,7 +720,6 @@ UserStore = context.UserStore renderCountered: () -> counterer = @counterer() myself = @myself() - initial = @neverAccepted() phrase = this.slotTimePhrase(this.counteredSlot()) action = `

Has suggested a different time for your lesson.

` @@ -677,7 +732,7 @@ UserStore = context.UserStore {detail} {this.slotMessage(this.counteredSlot())} - + ` }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee index da7d82223..fce89e76e 100644 --- a/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee @@ -9,6 +9,8 @@ # props.is_recurring # props.slot_decision # props.otherRole + # props.cancelErrors + # props.counterErrors mixins: [ ICheckMixin, @@ -27,14 +29,14 @@ componentWillMount: () -> @days = [] - @days.push(``) - @days.push(``) - @days.push(``) - @days.push(``) - @days.push(``) - @days.push(``) - @days.push(``) - @days.push(``) + @days.push(``) + @days.push(``) + @days.push(``) + @days.push(``) + @days.push(``) + @days.push(``) + @days.push(``) + @days.push(``) @hours = [] for hour in ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] @@ -110,11 +112,14 @@ render: () -> - if this.props.selfLastToAct - userPromptHeader = `

Would you like to do something else?

` + + + if (!this.props.initial && !this.props.counter) || this.props.selfLastToAct + userPromptHeader = `

Would you like to change this lesson?

` + messagePromptHeader = `

Send message to {this.props.otherRole} with your update.

` else userPromptHeader = `

How do you want to handle this request?

` - + messagePromptHeader = `

Send message to {this.props.otherRole} with your response.

` if this.props.slot_decision == 'counter' actionBtnText = "PROPOSE ALTERNATE TIME" else if this.props.slot_decision == 'decline' @@ -129,18 +134,27 @@ else actionBtnText = "ACCEPT & UPDATE LESSON" + counterClasses={field: true, 'slot-decision-field': true, error: this.props.counterErrors?} + + if this.props.counterErrors? + errorText = window.JK.reactErrors(this.props.counterErrors, {day_of_week: 'Day' }) + if this.props.is_recurring slotAltPrompt = `
Day: - + Time: : +
+ * Time will be local to {window.jstz.determine().name()} + {errorText}
+
` else slotAltPrompt = `
@@ -152,6 +166,9 @@ Time: : +
+ *Time will be local to {window.jstz.determine().name()} + {errorText}
` @@ -191,7 +208,7 @@
{slots} -
+
@@ -205,7 +222,7 @@
-

Send message to {this.props.otherRole} with your response.

+ {messagePromptHeader}
CANCEL diff --git a/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss b/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss index 9a7ae1d95..c52afb911 100644 --- a/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss +++ b/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss @@ -93,7 +93,7 @@ white-space: nowrap; float:left; display:block; - margin-bottom:10px; + //margin-bottom:10px; vertical-align: middle; line-height:31px; } @@ -139,6 +139,9 @@ width:auto; } } + .minute { + margin-right:20px; + } p { line-height:125% !important; font-size:14px !important; @@ -228,4 +231,9 @@ display:block; } + .error-text { + display: block; + //background-color: #600; + color: #f00; + } } \ No newline at end of file diff --git a/web/app/controllers/api_lesson_bookings_controller.rb b/web/app/controllers/api_lesson_bookings_controller.rb index 7e193b2c4..6ad88caef 100644 --- a/web/app/controllers/api_lesson_bookings_controller.rb +++ b/web/app/controllers/api_lesson_bookings_controller.rb @@ -128,14 +128,20 @@ class ApiLessonBookingsController < ApiController def accept next_lesson = @lesson_booking.next_lesson - next_lesson.accept({ + result = next_lesson.accept({ message: params[:message], slot: params[:slot], accepter: current_user }) - if next_lesson.errors.any? - respond_with_model next_lesson + if result.errors.any? + if result.is_a?(JamRuby::LessonBooking) + recursive_errors(result, [:lesson_booking_slots]) + elsif result.is_a?(JamRuby::LessonSession) + recursive_errors(result, [:lesson_booking_slots]) + else + raise "unknown response type in accept #{result.class}" + end return end @lesson_booking.reload @@ -148,7 +154,7 @@ class ApiLessonBookingsController < ApiController slot = LessonBookingSlot.new if @lesson_booking.recurring slot.slot_type = LessonBookingSlot::SLOT_TYPE_RECURRING - slot.day_of_week = slot[:day_of_week] + slot.day_of_week = params[:day_of_week] else slot.slot_type = LessonBookingSlot::SLOT_TYPE_SINGLE @@ -161,15 +167,22 @@ class ApiLessonBookingsController < ApiController slot.hour = params[:hour] slot.minute = params[:minute] slot.timezone = params[:timezone] + slot.update_all = params[:update_all] - next_lesson.counter({ + result = next_lesson.counter({ proposer: current_user, message: params[:message], slot: slot }) - if next_lesson.errors.any? - respond_with_model next_lesson + if result.errors.any? + if result.is_a?(JamRuby::LessonBooking) + recursive_errors(result, [:lesson_booking_slots]) + elsif result.is_a?(JamRuby::LessonSession) + recursive_errors(result, [:lesson_booking_slots]) + else + raise "unknown response type in counter #{result.class}" + end return end @lesson_booking.reload @@ -177,14 +190,20 @@ class ApiLessonBookingsController < ApiController def cancel next_lesson = @lesson_booking.next_lesson - next_lesson.cancel({ + result = next_lesson.cancel({ canceler: current_user, message: params[:message], update_all: true }) - if next_lesson.errors.any? - respond_with_model next_lesson + if result.errors.any? + if result.is_a?(JamRuby::LessonBooking) + recursive_errors(result, [:lesson_booking_slots]) + elsif result.is_a?(JamRuby::LessonSession) + recursive_errors(result, [:lesson_booking_slots]) + else + raise "unknown response type in cancel #{result.class}" + end return end @lesson_booking.reload diff --git a/web/app/views/api_lesson_bookings/show.rabl b/web/app/views/api_lesson_bookings/show.rabl index 5d482905c..84fc187b5 100644 --- a/web/app/views/api_lesson_bookings/show.rabl +++ b/web/app/views/api_lesson_bookings/show.rabl @@ -1,21 +1,21 @@ object @lesson_booking -attributes :id, :status, :lesson_type, :payment_style, :recurring, :teacher_id, :description, :lesson_length, :created_at, :user_id, :active, :accepter_id, :canceler_id, :cancel_message +attributes :id, :status, :lesson_type, :payment_style, :recurring, :teacher_id, :description, :lesson_length, :created_at, :user_id, :active, :accepter_id, :canceler_id, :cancel_message, :booked_price child(:lesson_booking_slots => :slots) { - attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created? + attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created?, :timezone } child(:default_slot => :default_slot) { - attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created? + attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created?, :timezone } child(:alt_slot => :alt_slot) { - attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created? + attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created?, :timezone } child(:counter_slot => :counter_slot) { - attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created? + attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created?, :timezone } diff --git a/web/app/views/api_users/notification_index.rabl b/web/app/views/api_users/notification_index.rabl index 23852b5fd..f9d8cdef1 100644 --- a/web/app/views/api_users/notification_index.rabl +++ b/web/app/views/api_users/notification_index.rabl @@ -1,6 +1,6 @@ collection @notifications -attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :band_id, :band_invitation_id, :formatted_msg, :message, :created_at +attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :band_id, :band_invitation_id, :formatted_msg, :message, :created_at, :lesson_session_id node :source_user do |n| source_user_data = {} diff --git a/web/lib/tasks/lesson.rake b/web/lib/tasks/lesson.rake index d5cb57ca4..92da6d0e2 100644 --- a/web/lib/tasks/lesson.rake +++ b/web/lib/tasks/lesson.rake @@ -2,11 +2,10 @@ require 'factory_girl' namespace :lessons do - task book_test_drive: :environment do |task, args| + task book_normal: :environment do |task, args| user = User.find_by_email(ENV['STUDENT_EMAIL']) teacher = User.find_by_email(ENV['TEACHER_EMAIL']) recurring = ENV['RECURRING'] == '1' - slots = [] if recurring @@ -18,6 +17,46 @@ namespace :lessons do end + + if user.stored_credit_card == false + user.stored_credit_card = true + user.save! + end + + if recurring + payment_style = LessonBooking::PAYMENT_STYLE_MONTHLY + else + payment_style = LessonBooking::PAYMENT_STYLE_SINGLE + end + + + booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", recurring, payment_style, 60) + if booking.errors.any? + puts booking.errors.inspect + raise "booking failed" + end + lesson = booking.lesson_sessions[0] + + + #lesson.accept({message: 'Yeah I got this', slot: slots[0]}) + #lesson.errors.any?.should be_false + #lesson.reload + #lesson.slot.should eql slots[0] + #lesson.status.should eql LessonSession::STATUS_APPROVED + + puts "http://localhost:3000/client#/jamclass/lesson-booking/#{booking.id}" + end + + task book_test_drive: :environment do |task, args| + user = User.find_by_email(ENV['STUDENT_EMAIL']) + teacher = User.find_by_email(ENV['TEACHER_EMAIL']) + + + slots = [] + + slots << FactoryGirl.build(:lesson_booking_slot_single, timezone: 'America/Chicago') + slots << FactoryGirl.build(:lesson_booking_slot_single, timezone: 'America/Chicago') + if user.stored_credit_card == false user.stored_credit_card = true user.save!