Merge branch 'develop' into feature/packages

This commit is contained in:
Seth Call 2016-05-31 19:43:52 -05:00
commit 1abacf0ec6
87 changed files with 1437 additions and 388 deletions

View File

@ -1,3 +1,4 @@
#!/usr/bin/env rake
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

View File

@ -0,0 +1,32 @@
ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
menu :label => 'User Campaigns', :parent => 'Users'
config.sort_order = 'created_at DESC'
config.batch_actions = false
config.clear_action_items!
config.filters = false
scope("Most Recent First", default: true) { |scope| scope.unscoped.order('created_at desc')}
index do
column "Email" do |user|
user.email
end
column "Bought TestDrive" do |user|
!user.most_recent_test_drive_purchase.nil? ? "Yes" : "No"
end
column "UTM Source" do |user|
user.origin_utm_source
end
column "UTM Medium" do |user|
user.origin_utm_medium
end
column "UTM Campaign" do |user|
user.origin_utm_campaign
end
column "Referrer" do |user|
user.origin_referrer
end
end
end

View File

@ -297,7 +297,10 @@ require "jam_ruby/models/affiliate_distribution"
require "jam_ruby/models/teacher_intent"
require "jam_ruby/models/school"
require "jam_ruby/models/school_invitation"
require "jam_ruby/models/teacher_instrument"
require "jam_ruby/models/teacher_subject"
require "jam_ruby/models/teacher_language"
require "jam_ruby/models/teacher_genre"
include Jampb
module JamRuby

View File

@ -1770,5 +1770,28 @@ module JamRuby
format.html { render :layout => "from_user_mailer" }
end
end
def lesson_attachment(sender, target, lesson_session, attachment)
@sender = sender
@target = target
@lesson_session = lesson_session
@attachment = attachment
email = target.email
@subject = "An attachment has been added to your lesson by #{sender.name}"
unique_args = {:type => "lesson_attachment"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@target.id])
mail(:to => email, :subject => @subject) do |format|
format.text
format.html { render :layout => "from_user_mailer" }
end
end
end
end

View File

@ -0,0 +1,20 @@
<% provide(:title, @subject) %>
<% provide(:photo_url, @sender.resolved_photo_url) %>
<% content_for :note do %>
<p>
<% if @attachment.is_a?(JamRuby::MusicNotation) %>
<% if @attachment.is_notation? %>
A music notation has been added to your lesson. You can download <a style="color:#fc0" href="<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>"><%= @attachment.file_name %></a> directly or at any time in the message window for this lesson.
<% else %>
A audio file has been added to your lesson. You can download <a style="color:#fc0" href="<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>"><%= @attachment.file_name %></a> directly or at any time in the message window for this lesson.
<% end %>
<% else %>
A recording named "<%= @attachment.name %>" has been added to your lesson. It can be viewed <a style="color:#fc0" href="<%= APP_CONFIG.external_root_url + "/recordings/#{@attachment.id}" %>">here</a> or found within the message window for this lesson.
<% end %>
<p>
<a href="<%= @lesson_session.web_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">VIEW LESSON DETAILS</a>
</p>
<% end %>

View File

@ -0,0 +1,12 @@
<% if @attachment.is_a?(JamRuby::MusicNotation) %>
<% if @attachment.is_notation? %>
A music notation has been added to your lesson. You can download (<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>">)<%= @attachment.file_name %> directly or at any time in the message window for this lesson.
<% else %>
A audio file has been added to your lesson. You can download (<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>">)<%= @attachment.file_name %> directly or at any time in the message window for this lesson.
<% end %>
<% else %>
A recording named "<%= @attachment.name %>" has been added to your lesson. It can be viewed (<%= APP_CONFIG.external_root_url + "/recordings/#{@attachment.id}" %>">) here or found within the message window for this lesson.
<% end %>
VIEW LESSON DETAILS (<%= @lesson_session.web_url %>)

View File

@ -16,7 +16,8 @@ module JamRuby
has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres"
# teachers
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_genres"
has_many :teachers, :class_name => "JamRuby::Teacher", :through => :teachers_genres
has_many :teachers_genres, :class_name => "JamRuby::TeacherGenre"
# jam tracks
has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "genre_id"

View File

@ -44,7 +44,8 @@ module JamRuby
has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::ActiveMusicSession", :join_table => "genres_music_sessions"
# teachers
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_instruments"
has_many :teachers, :class_name => "JamRuby::Teacher", through: :teachers_instruments
has_many :teachers_instruments, class_name: "JamRuby::TeacherInstrument"
def self.standard_list
return Instrument.where('instruments.popularity > 0').order('instruments.description ASC')

View File

@ -2,7 +2,8 @@ module JamRuby
class Language < ActiveRecord::Base
include HtmlSanitize
html_sanitize strict: [:name, :description]
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_languages"
has_many :teachers, :class_name => "JamRuby::Teacher", :through => :teachers_languages
has_many :teachers_languages, class_name: "JamRuby::TeacherLanguage"
def self.english_sort
languages = Language.order(:description)

View File

@ -243,7 +243,12 @@ module JamRuby
minimum_start_time = create_minimum_booking_time
# get all sessions that are already scheduled for this booking ahead of the minimum time
sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).order(:created_at)
sessions= MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start is not null").order(:created_at)
if recurring
# only want times ahead of this for recurring
sessions = sessions.where("scheduled_start > ?", minimum_start_time)
end
if @default_slot_did_change
# # adjust all session times
@ -421,6 +426,9 @@ module JamRuby
self.errors.add(:status, "This lesson is already #{self.status}.")
end
if self.accepter.nil?
self.errors.add(:accepter, "No one has been indicated as accepting the lesson")
end
self.accepting = false
end

View File

@ -33,6 +33,9 @@ module JamRuby
validate :validate_proposer
before_validation :before_validation
def is_recurring?
slot_type == SLOT_TYPE_RECURRING
end
def before_validation
if proposer.nil?
self.proposer = container.student
@ -81,6 +84,7 @@ module JamRuby
candidate = scheduled_time(i + week_offset)
#puts "#{i}: candidate #{candidate} week_offset:#{week_offset}"
#puts "DAY_OF_WEEK #{day_of_week}"
if day_of_week && candidate <= minimum_start_time
# move it up a week
week_offset += 1
@ -173,7 +177,7 @@ module JamRuby
duration = lesson_length * 60 # convert from minutes to seconds
end_time = start_time + duration
if with_timezone
"#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{tz.pretty_name}"
"#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{tz.pretty_name})"
else
"#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}"
end

View File

@ -5,7 +5,7 @@ module JamRuby
include HtmlSanitize
html_sanitize strict: [:cancel_message]
attr_accessor :accepting, :creating, :countering, :autocanceling, :countered_slot, :countered_lesson, :canceling, :assigned_student
attr_accessor :accepting, :creating, :countering, :countering_flag, :autocanceling, :countered_slot, :countered_lesson, :canceling, :assigned_student
@@log = Logging.logger[LessonSession]
@ -61,6 +61,7 @@ module JamRuby
validates :post_processed, inclusion: {in: [true, false]}
validate :validate_creating, :if => :creating
validate :validate_countering, :if => :countering_flag
validate :validate_accepted, :if => :accepting
validate :validate_canceled, :if => :canceling
validate :validate_autocancel, :if => :autocanceling
@ -165,7 +166,7 @@ module JamRuby
end
# test drives don't have a lesson_payment_charge, so we don't join against them
MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where('lesson_sessions.lesson_type = ?', LESSON_TYPE_TEST_DRIVE).where("session_removed_at IS NOT NULL OR ? > scheduled_start + (INTERVAL '1 minutes' * duration)", Time.now).where('analysed = true').where('lesson_sessions.post_processed = false').each do |music_session|
MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where('lesson_sessions.lesson_type = ?', LESSON_TYPE_TEST_DRIVE).where("? > scheduled_start + (INTERVAL '1 minutes' * duration)", Time.now).where('analysed = true').where('lesson_sessions.post_processed = false').each do |music_session|
lession_session = music_session.lesson_session
lession_session.session_completed
end
@ -366,7 +367,9 @@ module JamRuby
end
def recurring_completed
puts "RECURRING COMPLETED #{success}"
if success
if lesson_booking.is_monthly_payment?
# monthly payments are handled at beginning of month; just poke with email, and move on
@ -399,6 +402,7 @@ module JamRuby
end
else
puts "STUDENT NO BILL SENT #{self.id}"
if !sent_notices
if !school_on_school?
# bad session; just poke user
@ -422,6 +426,7 @@ module JamRuby
else
if !sent_notices
if !school_on_school?
puts "STUDENT NO BILL SENT #{success}"
UserMailer.student_lesson_normal_no_bill(self).deliver
UserMailer.teacher_lesson_normal_no_bill(self).deliver
end
@ -501,6 +506,22 @@ module JamRuby
end
end
def validate_countering
if counter_slot.nil?
errors.add(:counter_slot, "must be specified")
elsif !approved_before? && (status == STATUS_REQUESTED || status == STATUS_COUNTERED)
if recurring && !counter_slot.update_all
errors.add(:counter_slot, "Only 'update all' counter-proposals are allowed for un-approved, recurring lessons")
end
if recurring && !counter_slot.is_recurring?
errors.add(:counter_slot, "Only 'recurring' counter-proposals are allowed for un-approved, recurring lessons")
end
end
self.countering_flag = false
end
def validate_accepted
if self.status_was != STATUS_REQUESTED && self.status_was != STATUS_COUNTERED
self.errors.add(:status, "This session is already #{self.status_was}.")
@ -787,6 +808,7 @@ module JamRuby
update_all = slot.update_all || !lesson_booking.recurring
self.countering = true
self.countering_flag = true
slot.proposer = proposer
slot.lesson_session = self
slot.message = message
@ -796,11 +818,12 @@ module JamRuby
self.countered_slot = slot
self.countered_lesson = self
self.status = STATUS_COUNTERED
if !update_all
#if !update_all
self.counter_slot = slot
end
#end
if self.save
if update_all && !lesson_booking.counter(self, proposer, slot)
#if update_all && !lesson_booking.counter(self, proposer, slot)
if !lesson_booking.counter(self, proposer, slot)
response = lesson_booking
raise ActiveRecord::Rollback
end

View File

@ -48,6 +48,9 @@ module JamRuby
s3_manager.sign_url(self[:file_url], {:expires => expiration_time, :secure => true})
end
def is_notation?
self.attachment_type == TYPE_NOTATION
end
private
def self.construct_filename(notation)

View File

@ -2,6 +2,7 @@ module JamRuby
class Subject < ActiveRecord::Base
include HtmlSanitize
html_sanitize strict: [:name, :description]
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_subjects"
has_many :teachers, :class_name => "JamRuby::Teacher", :through => :teachers_subjects
has_many :teachers_subjects, class_name: "JamRuby::TeacherSubject"
end
end

View File

@ -4,10 +4,14 @@ module JamRuby
html_sanitize strict: [:biography, :website]
attr_accessor :validate_introduction, :validate_basics, :validate_pricing
attr_accessible :genres, :teacher_experiences, :experiences_teaching, :experiences_education, :experiences_award
has_and_belongs_to_many :genres, :class_name => "JamRuby::Genre", :join_table => "teachers_genres", :order => "description"
has_and_belongs_to_many :instruments, :class_name => "JamRuby::Instrument", :join_table => "teachers_instruments", :order => "description"
has_and_belongs_to_many :subjects, :class_name => "JamRuby::Subject", :join_table => "teachers_subjects", :order => "description"
has_and_belongs_to_many :languages, :class_name => "JamRuby::Language", :join_table => "teachers_languages", :order => "description"
has_many :genres, :class_name => "JamRuby::Genre", :through => :teachers_genres # , :order => "description"
has_many :teachers_genres, :class_name => "JamRuby::TeacherGenre"
has_many :instruments, :class_name => "JamRuby::Instrument", through: :teachers_instruments # , :order => "description"
has_many :teachers_instruments, class_name: "JamRuby::TeacherInstrument"
has_many :subjects, :class_name => "JamRuby::Subject", :through => :teachers_subjects # , :order => "description"
has_many :teachers_subjects, class_name: "JamRuby::TeacherSubject"
has_many :languages, :class_name => "JamRuby::Language", :through => :teachers_languages # , :order => "description"
has_many :teachers_languages, class_name: "JamRuby::TeacherLanguage"
has_many :teacher_experiences, :class_name => "JamRuby::TeacherExperience"
has_many :experiences_teaching, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'teaching'}
has_many :experiences_education, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'education'}
@ -36,7 +40,7 @@ module JamRuby
validate :offer_duration, :if => :validate_pricing
validate :teaches_ages, :if => :validate_basics
default_scope { includes(:genres).order('created_at desc') }
#default_scope { includes(:genres).order('created_at desc') }
after_save :update_profile_pct
@ -53,7 +57,7 @@ module JamRuby
limit ||= 20
limit = limit.to_i
query = User.joins(:teacher)
query = User.unscoped.joins(:teacher)
# only show teachers with ready for session set to true
query = query.where('teachers.ready_for_session_at IS NOT NULL')

View File

@ -0,0 +1,11 @@
module JamRuby
class TeacherGenre < ActiveRecord::Base
self.table_name = "teachers_genres"
belongs_to :teacher, class_name: "JamRuby::Teacher"
belongs_to :genre, class_name: "JamRuby::Genre"
validates :teacher, presence:true
validates :genre, presence: true
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class TeacherInstrument < ActiveRecord::Base
self.table_name = "teachers_instruments"
belongs_to :teacher, class_name: "JamRuby::Teacher"
belongs_to :instrument, class_name: "JamRuby::Instrument"
validates :teacher, presence:true
validates :instrument, presence: true
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class TeacherLanguage < ActiveRecord::Base
self.table_name = "teachers_languages"
belongs_to :teacher, class_name: "JamRuby::Teacher"
belongs_to :language, class_name: "JamRuby::Language"
validates :teacher, presence:true
validates :language, presence: true
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class TeacherSubject < ActiveRecord::Base
self.table_name = "teachers_subjects"
belongs_to :teacher, class_name: "JamRuby::Teacher"
belongs_to :subject, class_name: "JamRuby::Subject"
validates :teacher, presence:true
validates :subject, presence: true
end
end

View File

@ -51,7 +51,7 @@ module JamRuby
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"
has_many :reviews, :class_name => "JamRuby::Review"
has_one :review_summary, :class_name => "JamRuby::ReviewSummary"
has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target
# calendars (for scheduling NOT in music_session)
has_many :calendars, :class_name => "JamRuby::Calendar"
@ -2090,7 +2090,7 @@ module JamRuby
LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_PAID).first
end
def most_recent_test_drive_purchase
def most_recent_test_drive_purchase
lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).order('created_at desc').first
end

View File

@ -1057,5 +1057,13 @@ FactoryGirl.define do
factory :email_blacklist, class: "JamRuby::EmailBlacklist" do
sequence(:email) { |n| "person_#{n}@example.com"}
end
factory :music_notation, class: "JamRuby::MusicNotation" do
attachment_type {JamRuby::MusicNotation::TYPE_NOTATION}
association :user, factory: :user
file_url 'abc'
size 100
file_name 'some_file.jpg'
end
end

View File

@ -42,7 +42,6 @@ describe "Monthly Recurring Lesson Flow" do
########## Need validate their credit card
token = create_stripe_token
result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id})
puts "result #{result.inspect}"
booking.reload
booking.card_presumed_ok.should be_true
booking.errors.any?.should be_false
@ -118,7 +117,7 @@ describe "Monthly Recurring Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user})
UserMailer.deliveries.each do |del|
# puts del.inspect
end
@ -203,7 +202,7 @@ describe "Monthly Recurring Lesson Flow" do
payment = TeacherPayment.first
payment.amount_in_cents.should eql 3000
payment.fee_in_cents.should eql (3000 * 0.28).round
payment.teacher_payment_charge.amount_in_cents.should eql 3000
payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round
payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql teacher_distribution
@ -320,7 +319,7 @@ describe "Monthly Recurring Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user})
UserMailer.deliveries.each do |del|
# puts del.inspect
end
@ -419,6 +418,7 @@ describe "Monthly Recurring Lesson Flow" do
it "affiliate gets their cut" do
Timecop.travel(2016, 05, 15)
user.affiliate_referral = affiliate_partner
user.save!
teacher_user.affiliate_referral = affiliate_partner2
@ -429,7 +429,6 @@ describe "Monthly Recurring Lesson Flow" do
user.reload
puts "user.lesson_purchases #{user.lesson_purchases}"
user.lesson_purchases.count.should eql 1
lesson_package_purchase = user.lesson_purchases.first
teacher_distribution = lesson_package_purchase.teacher_distribution
@ -447,6 +446,7 @@ describe "Monthly Recurring Lesson Flow" do
end
it "school affiliate gets nothing when teacher school is involved" do
Timecop.travel(2016, 05, 15)
teacher.school = school
teacher.save!
@ -469,6 +469,8 @@ describe "Monthly Recurring Lesson Flow" do
end
it "student school affiliates gets cut when student school is involved. so does teacher's" do
# in the middle of the month so that we don't get the next month's in-advance purchase put on us
Timecop.travel(2016, 05, 15)
user.affiliate_referral = school.affiliate_partner
user.save!

View File

@ -68,7 +68,7 @@ describe "Normal Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false, accepter: teacher_user})
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.slot.should eql booking.default_slot
@ -251,7 +251,7 @@ describe "Normal Lesson Flow" do
payment = TeacherPayment.first
payment.amount_in_cents.should eql 3000
payment.fee_in_cents.should eql (3000 * 0.28).round
payment.teacher_payment_charge.amount_in_cents.should eql 3000
payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round
payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql teacher_distribution
@ -352,7 +352,7 @@ describe "Normal Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false,accepter: teacher_user})
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.slot.should eql student_counter
@ -508,7 +508,7 @@ describe "Normal Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user})
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.slot.should eql student_counter

View File

@ -55,7 +55,7 @@ describe "Recurring Lesson Flow" do
booking.status.should eql LessonBooking::STATUS_REQUESTED
######### Teacher counters with new slot
teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14, update_all: true)
teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 14, update_all: true)
UserMailer.deliveries.clear
lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'})
booking.reload
@ -81,7 +81,7 @@ describe "Recurring Lesson Flow" do
#notification.message.should eql "Instructor has proposed a different time for your lesson."
######### Student counters with new slot
student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16, update_all: true)
student_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, hour: 16, update_all: true)
UserMailer.deliveries.clear
lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'})
lesson_session.errors.any?.should be false
@ -105,7 +105,7 @@ describe "Recurring Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, accepter: teacher_user})
UserMailer.deliveries.each do |del|
# puts del.inspect
end
@ -132,7 +132,11 @@ describe "Recurring Lesson Flow" do
notification.student_directed.should eql true
notification.purpose.should eql 'accept'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
user.reload
user.sales.length.should eql 0
booking.reload
booking.lesson_sessions[0].scheduled_start.should_not eql booking.lesson_sessions[1].scheduled_start
# teacher & student get into session
start = lesson_session.scheduled_start
@ -181,6 +185,9 @@ describe "Recurring Lesson Flow" do
lesson_session.sent_billing_notices.should be true
user.reload
user.remaining_test_drives.should eql 0
UserMailer.deliveries.each do |d|
puts d.subject
end
UserMailer.deliveries.length.should eql 2 # one for student, one for teacher
end
end

View File

@ -140,7 +140,7 @@ describe "TestDrive Lesson Flow" do
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user})
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.slot.should eql student_counter
@ -237,7 +237,7 @@ describe "TestDrive Lesson Flow" do
teacher_distribution.ready.should be_true
teacher_distribution.distributed.should be_true
teacher_payment.teacher_payment_charge.amount_in_cents.should eql 1000
teacher_payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round
teacher_payment.teacher_payment_charge.fee_in_cents.should eql 0
user.sales.count.should eql 1

View File

@ -741,6 +741,14 @@ describe LessonBooking do
UserMailer.deliveries.clear
Timecop.freeze(7.days.ago)
mailer = mock
mailer.should_receive(:deliver).exactly(2).times
UserMailer.should_receive(:student_lesson_booking_canceled).and_return(mailer)
UserMailer.should_receive(:teacher_lesson_booking_canceled).and_return(mailer)
UserMailer.should_receive(:student_lesson_canceled).exactly(0).times
UserMailer.should_receive(:teacher_lesson_canceled).exactly(0).times
lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: true})
lesson_session.errors.any?.should be_false
lesson_session.reload
@ -748,14 +756,32 @@ describe LessonBooking do
booking.reload
booking.status.should eql LessonSession::STATUS_CANCELED
booking.canceler.should eql user
UserMailer.deliveries.length.should eql 2
end
end
describe "rescheduling" do
after do
Timecop.return
it "initial slot is in the past" do
booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)
lesson_session = booking.lesson_sessions[0]
initial_scheduled_time = lesson_session.scheduled_start
counter = FactoryGirl.build(:lesson_booking_slot_single, preferred_day: Date.today + 20)
lesson_session.counter({proposer: user, slot: counter, message: 'ACtually, let\'s do this instead for just this one'})
Timecop.travel(initial_scheduled_time + 1)
lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter, update_all: false})
booking.reload
booking.status.should eql LessonBooking::STATUS_APPROVED
booking.lesson_sessions.count.should eql 1
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.status.should eql LessonSession::STATUS_APPROVED
lesson_session.scheduled_start.should eql counter.scheduled_time(0)
end
it "non-recurring, accepted with new slot" do
booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)
lesson_session = booking.lesson_sessions[0]

View File

@ -10,6 +10,63 @@ describe LessonSession do
let(:lesson_booking) {b = LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60); b.card_presumed_ok = true; b.save!; b}
let(:lesson_session) {lesson_booking.lesson_sessions[0]}
describe "counter" do
describe "recurring" do
it "counter madness" do
lesson = monthly_lesson(user, teacher, {accept:false})
# start with the student
invalid = FactoryGirl.build(:lesson_booking_slot_single, update_all: false)
lesson.counter({proposer: user, message: "crumble and bumble", slot: invalid})
lesson.errors.any?.should be_true
lesson.errors[:counter_slot].should eql ["Only 'update all' counter-proposals are allowed for un-approved, recurring lessons", "Only 'recurring' counter-proposals are allowed for un-approved, recurring lessons"]
lesson.reload
lesson.counter_slot.should be_nil
counter1 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: true)
lesson.counter({proposer: user, message: "crumble and bumble take 2", slot: counter1})
lesson.errors.any?.should be_false
lesson.reload
lesson.status.should eql LessonSession::STATUS_COUNTERED
lesson.counter_slot.id.should eql counter1.id
lesson.lesson_booking.counter_slot.id.should eql counter1.id
counter2 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: true)
lesson.counter({proposer: teacher, message: "crumble and bumble take 3", slot: counter2})
lesson.errors.any?.should be_false
lesson.reload
lesson.status.should eql LessonSession::STATUS_COUNTERED
lesson.counter_slot.id.should eql counter2.id
lesson.lesson_booking.counter_slot.id.should eql counter2.id
lesson.accept({accepter: user, message: "burp", slot: counter2})
lesson.errors.any?.should be_false
lesson.reload
lesson.status.should eql LessonSession::STATUS_APPROVED
counter3 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: false)
lesson.counter({proposer: user, message: "crumble and bumble take 4", slot: counter3})
lesson.errors.any?.should be_false
lesson.reload
lesson.status.should eql LessonSession::STATUS_COUNTERED
lesson.counter_slot.id.should eql counter3.id
lesson.lesson_booking.counter_slot.id.should eql counter3.id
counter4 = FactoryGirl.build(:lesson_booking_slot_recurring, update_all: true)
lesson.counter({proposer: teacher, message: "crumble and bumble take 5", slot: counter4})
lesson.errors.any?.should be_false
lesson.reload
lesson.status.should eql LessonSession::STATUS_COUNTERED
lesson.counter_slot.id.should eql counter4.id
lesson.lesson_booking.counter_slot.id.should eql counter4.id
end
end
end
describe "autocancel" do
it "can't autocancel in the past" do
lesson_session.status.should eql LessonSession::STATUS_REQUESTED

View File

@ -45,18 +45,20 @@ describe Teacher do
it "instruments" do
teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now)
teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query]
teachers.length.should eq 0
#teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query]
#teachers.length.should eq 0
teacher.instruments << Instrument.find('acoustic guitar')
teacher.save!
teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query]
teachers.length.should eq 1
teachers[0].should eq(teacher.user)
#teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query]
#teachers.length.should eq 1
#teachers[0].should eq(teacher.user)
teacher.instruments << Instrument.find('electric guitar')
teacher.save!
#teacher.instruments << Instrument.find('electric guitar')
#teacher.save!
puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query]
puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!---"
teachers.length.should eq 1
teachers[0].should eq(teacher.user)
end

View File

@ -9,6 +9,7 @@ describe GoogleAnalyticsEvent do
end
describe "track band analytics" do
pending "job is commented out"
it 'reports first recording' do
ResqueSpec.reset!
user = FactoryGirl.create(:user)
@ -26,6 +27,7 @@ describe GoogleAnalyticsEvent do
end
it 'reports first real session' do
pending "job is commented out"
ResqueSpec.reset!
JamRuby::GoogleAnalyticsEvent::BandSessionTracker.should have_schedule_size_of(0)
user = FactoryGirl.create(:user)
@ -72,6 +74,7 @@ describe GoogleAnalyticsEvent do
ResqueSpec.reset!
end
it 'reports size increment' do
pending "job is commented out"
user = FactoryGirl.create(:user)
music_session = FactoryGirl.create(:active_music_session,
:creator => user,
@ -86,6 +89,7 @@ describe GoogleAnalyticsEvent do
end
it 'reports duration' do
pending "job is commented out"
user = FactoryGirl.create(:user)
JamRuby::GoogleAnalyticsEvent::SessionDurationTracker.should have_schedule_size_of(0)
music_session = FactoryGirl.create(:active_music_session,

View File

@ -162,6 +162,26 @@ describe "RenderMailers", :slow => true do
UserMailer.deliveries.clear
UserMailer.lesson_starting_soon_student(lesson).deliver
end
it "music_notation_attachment" do
@filename = "music_notation_attachment"
lesson = testdrive_lesson(user, teacher)
UserMailer.deliveries.clear
notation = FactoryGirl.create(:music_notation, user: user)
UserMailer.lesson_attachment(user, teacher, lesson, notation).deliver
end
it "recording_attachment" do
@filename = "recording_attachment"
lesson = testdrive_lesson(user, teacher)
UserMailer.deliveries.clear
claim = FactoryGirl.create(:claimed_recording, user: user)
UserMailer.lesson_attachment(user, teacher, lesson, claim).deliver
end
end
end

View File

@ -38,7 +38,7 @@ require 'timecop'
require 'resque_spec/scheduler'
# uncomment this to see active record logs
# ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)
ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)
include JamRuby

View File

@ -65,7 +65,6 @@ def book_lesson(user, teacher, options)
LessonPackagePurchase.create(user, booking, LessonPackageType.package_for_test_drive_count(options[:package_count]))
end
elsif options[:monthly]
puts "did it"
LessonPackagePurchase.create(user, booking, LessonPackageType.single, Date.today.year, Date.today.month)
end
@ -83,7 +82,7 @@ def book_lesson(user, teacher, options)
end
if options[:accept]
lesson.accept({message: 'Yeah I got this', slot: slots[0]})
lesson.accept({message: 'Yeah I got this', slot: slots[0], accepter: teacher})
lesson.errors.any?.should be_false
lesson.reload
lesson.slot.should eql slots[0]

View File

@ -581,6 +581,9 @@
if(!defaults.show_checkbox) {
$feedItem.find('.select-box').hide();
}
else {
context.JK.checkbox($feedItem.find('.select-box'))
}
if(defaults.hide_avatar) {
$feedItem.find('.avatar-small.ib').hide();
}
@ -598,6 +601,7 @@
$feedItem.data('original-max-height', $feedItem.css('height'));
context.JK.bindHoverEvents($feedItem);
context.JK.bindProfileClickEvents($feedItem);
context.JK.popExternalLinks($feedItem)
}
else {
logger.warn("skipping feed type: " + feed.type);

View File

@ -99,6 +99,10 @@
ioTargetFail : 'ioTargetFail'
}
var jamClassReasons = {
testDrive: 'TestDrive'
}
var networkTestFailReasons = {
stun : 'STUN',
bandwidth : 'Bandwidth',
@ -129,7 +133,8 @@
jkFollow : 'jkFollow',
jkFavorite : 'jkFavorite',
jkComment : 'jkComment',
fileDownload: "DownloadFile"
fileDownload: "DownloadFile",
jamclass: 'JamClass'
};
// JamTrack categories and actions:
@ -204,6 +209,11 @@
context.ga('send', 'event', categories.register, action, registrationType);
}
function trackTestDrivePurchase(count) {
context.ga('send', 'event', categories.jamclass, jamClassReasons.testDrive, count);
}
function trackDownload(platform) {
var normalizedPlatform = translatePlatformForGA(platform);
@ -490,6 +500,7 @@
GA.virtualPageView = virtualPageView;
GA.trackTiming = trackTiming;
GA.trackFileDownload = trackFileDownload;
GA.trackTestDrivePurchase = trackTestDrivePurchase;
context.JK.GA = GA;

View File

@ -228,7 +228,7 @@
"Electric Guitar": { "client_id": 50, "server_id": "electric guitar" },
"Keyboard": { "client_id": 60, "server_id": "keyboard" },
"Piano": { "client_id": 61, "server_id": "piano" },
"Upright Bass": { "client_id": 62, "server_id": "upright bass" },
"Upright Bass": { "client_id": 62, "server_id": "double bass" },
"Voice": { "client_id": 70, "server_id": "voice" },
"Flute": { "client_id": 80, "server_id": "flute" },
"Clarinet": { "client_id": 90, "server_id": "clarinet" },
@ -250,6 +250,9 @@
"Other": { "client_id": 250, "server_id": "other" }
};
context.JK.client_to_server_instrument_map = {
10: { "server_id": "acoustic guitar" },
20: { "server_id": "bass guitar" },
@ -259,7 +262,7 @@
50: { "server_id": "electric guitar" },
60: { "server_id": "keyboard" },
61: { "server_id": "piano"} ,
62: { "server_id": "upright bass"} ,
62: { "server_id": "double bass"} ,
70: { "server_id": "voice" },
80: { "server_id": "flute" },
90: { "server_id": "clarinet" },
@ -283,10 +286,21 @@
context.JK.instrument_id_to_instrument = {};
context.JK.server_to_client_instrument_alpha = [];
(function() {
$.each(context.JK.server_to_client_instrument_map, function(key, value) {
context.JK.instrument_id_to_instrument[value.server_id] = { client_id: value.client_id, display: key }
context.JK.server_to_client_instrument_alpha.push({ client_id: value.client_id, display: key, server_id: value.server_id })
});
context.JK.server_to_client_instrument_alpha.sort(function(a, b){
if ( a.display < b.display )
return -1;
if ( a.display > b.display )
return 1;
return 0;
});
})();

View File

@ -82,7 +82,7 @@
}
function subtlePulse($element) {
$element.find('.bt-content').pulse({'background-color' : '#868686'}, {pulses: 3}, function() { $element.css('background-color', '#980006')})
$element.find('.bt-content').pulse({'background-color' : '#868686'}, {pulses: 2, duration: 1000, interval:300}, function() { $element.css('background-color', '#980006')})
}
helpBubble.rotateJamTrackLandingBubbles = function($preview, $video, $ctaButton, $alternativeCta) {
@ -203,7 +203,7 @@
helpBubble.showBuyTestDrive = function($element, $offsetParent, user, callback) {
return context.JK.onceBubble($element, 'side-buy-test-drive', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) {
subtlePulse(container)
var $bookNow = container('a.book-now')
var $bookNow = container.find('a.book-now')
$bookNow.off('click').on('click', function(e) {
e.preventDefault()
callback()

View File

@ -24,7 +24,7 @@ AttachmentStore = context.AttachmentStore
notationUploadDone: () ->
logger.debug("AttachmentStatus: notationUploadDone")
context.JK.Banner.showNotice('Notation Uploaded', 'The music notation file has been uploaded, and can be accessed from the Messages window for this lesson.')
#context.JK.Banner.showNotice('Notation Uploaded', 'The music notation file has been uploaded, and can be accessed from the Messages window for this lesson.')
notationUploadFail: () ->
logger.debug("AttachmentStatus: notationUploadFail")
@ -32,11 +32,11 @@ AttachmentStore = context.AttachmentStore
audioSelected: (e) ->
files = $(e.target).get(0).files
logger.debug("audio files selected: ", files)
window.AttachmentActions.uploadAudio.trigger(files, @notationUploadDone, @notationUploadFail)
window.AttachmentActions.uploadAudios.trigger(files, @notationUploadDone, @notationUploadFail)
audioUploadDone: () ->
logger.debug("AttachmentStatus: audioUploadDone")
context.JK.Banner.showNotice('Audio file Uploaded', 'The audio file has been uploaded, and can be accessed from the Messages window for this lesson.')
#context.JK.Banner.showNotice('Audio file Uploaded', 'The audio file has been uploaded, and can be accessed from the Messages window for this lesson.')
audioUploadFail: () ->
logger.debug("AttachmentStatus: audioUploadFail")

View File

@ -1,3 +1,4 @@
context = window
rest = context.JK.Rest()
logger = context.JK.logger

View File

@ -125,6 +125,7 @@ ChatActions = @ChatActions
else
purpose = null
additional = null
if msg.purpose == 'Notation File'
additional = `<a className="additional" onClick={this.notationClicked.bind(this, msg.music_notation)}>{msg.music_notation.file_name}</a>`
else if msg.purpose == 'Audio File'

View File

@ -44,7 +44,7 @@ logger = context.JK.logger
for object in this.props.sourceObjects
nm = "check_#{object.id}"
checked = @isChecked(object.id)
object_options.push `<div className='checkItem'><input type='checkbox' key={object.id} name={nm} data-object-id={object.id} checked={checked}></input><label htmlFor={nm}>{object.description}</label></div>`
object_options.push `<div className='checkItem'><input type='checkbox' key={object.id} name={nm} data-object-id={object.id} checked={checked}></input><label htmlFor={nm}>{object.description}</label><br className="clearall"/></div>`
`<div className="CheckBoxList react-component">
<div className="checkbox-scroller left">

View File

@ -29,8 +29,8 @@ ConfigureTracksStore = @ConfigureTracksStore
instruments = []
instruments.push(`<option key="" value="">Select the instrument for this track</option>`)
for displayName, value of context.JK.server_to_client_instrument_map
instruments.push(`<option key={value.server_id} value={value.server_id}>{displayName}</option>`)
for instrument in context.JK.server_to_client_instrument_alpha
instruments.push(`<option key={instrument.server_id} value={instrument.server_id}>{instrument.display}</option>`)
vsts = []
@ -134,8 +134,8 @@ ConfigureTracksStore = @ConfigureTracksStore
midiInstruments = []
instruments = []
for displayName, value of context.JK.server_to_client_instrument_map
instruments.push(`<option key={value.server_id} value={value.server_id}>{displayName}</option>`)
for instrument in context.JK.server_to_client_instrument_alpha
instruments.push(`<option key={instrument.server_id} value={instrument.server_id}>{instrument.display}</option>`)
selectedMidiInterface = ''
selectedInstrument = context.JK.client_to_server_instrument_map[50].server_id # default to electric guitar

View File

@ -129,9 +129,9 @@ LessonTimerActions = context.LessonTimerActions
rest.checkLessonReschedule({id: lesson.id, update_all: recurring})
.done((response) => (
if recurring
window.location.href = '/client#/jamclass/lesson-booking/' + lesson.lesson_booking_id
window.location.href = '/client#/jamclass/lesson-booking/' + lesson.lesson_booking_id + "_rescheduling"
else
window.location.href = '/client#/jamclass/lesson-booking/' + lesson.id
window.location.href = '/client#/jamclass/lesson-booking/' + lesson.id + "_rescheduling"
))
.fail((jqXHR) => (
if jqXHR.status == 422
@ -179,7 +179,13 @@ LessonTimerActions = context.LessonTimerActions
context.JK.Banner.showAlert('late cancellation warning',
'Cancelling a lesson less than 24 hours before its scheduled to start should be avoided, as its an inconvenience to the student. Repeated violations of this policy will negatively affect your teacher score.')
@refreshLesson(lesson.id)
lessonsFromBooking = []
for check in @lessons()
if check.lesson_booking_id == lesson.lesson_booking_id
lessonsFromBooking.push(check)
for check in lessonsFromBooking
@refreshLesson(check.id)
cancelLessonBookingFail: (jqXHR) ->
@app.ajaxError(jqXHR)
@ -224,12 +230,12 @@ LessonTimerActions = context.LessonTimerActions
if lesson.recurring
buttons = []
buttons.push({
name: 'THIS SESSION',
name: 'THIS LESSON',
buttonStyle: 'button-orange',
click: (() => (@rescheduleSelected(lesson, false)))
})
buttons.push({
name: 'ALL SESSIONS',
name: 'ALL LESSONS',
buttonStyle: 'button-orange',
click: (() => (@rescheduleSelected(lesson, true)))
})
@ -251,13 +257,13 @@ LessonTimerActions = context.LessonTimerActions
verbLower = 'cancel'
if !lesson.isRequested || lesson.recurring
buttons = []
buttons.push({name: 'CANCEL', buttonStyle: 'button-grey'})
buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
buttons.push({
name: 'THIS SESSION',
name: 'CANCEL THIS LESSON',
buttonStyle: 'button-orange',
click: (() => (@cancelSelected(lesson, false)))
})
buttons.push({name: 'ALL SESSIONS', buttonStyle: 'button-orange', click: (() => (@cancelSelected(lesson, true)))})
buttons.push({name: 'CANCEL ALL LESSONS', buttonStyle: 'button-orange', click: (() => (@cancelSelected(lesson, true)))})
context.JK.Banner.show({
title: 'Select One',
html: "Do you wish to all #{verbLower} all lessons or just the one selected?",
@ -521,7 +527,10 @@ LessonTimerActions = context.LessonTimerActions
else
unreadMessages = null
timeStmt = lessonData.music_session.pretty_scheduled_start_with_timezone
if lessonData.status == 'countered'
timeStmt = lessonData.counter_slot.pretty_scheduled_start_with_timezone
else
timeStmt = lessonData.music_session.pretty_scheduled_start_with_timezone
if lessonData.times? && lessonData.displayStatus == 'Scheduled'
if lessonData.times.startingSoon

View File

@ -0,0 +1,135 @@
@JamClassSearchHeader = React.createClass({
mixins: [Reflux.listenTo(@UserStore, "onUserChanged"), Reflux.listenTo(@TeacherSearchStore, "onTeacherSearchStore")]
onTeacherSearchStore: ()->
getInitialState: () ->
{user: null}
onUserChanged: (@user) ->
@setState({user: @user?.user})
createSearchDescription: () ->
searchOptions = TeacherSearchStore.getState()
summary = ''
if searchOptions.onlyMySchool && @state.user?.school_id?
summary += "From My School Only"
instruments = searchOptions.instruments
if instruments? && instruments.length > 0
if instruments.length == 1
bit = "Instrument = #{InstrumentStore.display(instruments[0])}"
else
instruments.length > 1
bit = "Instruments = #{InstrumentStore.display(instruments[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
subjects = searchOptions.subjects
if subjects? && subjects.length > 0
if subjects.length == 1
bit = "Subject = #{SubjectStore.display(subjects[0])}"
else
subjects.length > 1
bit = "Subjects = #{SubjectStore.display(subjects[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
genres = searchOptions.genres
if genres? && genres.length > 0
if genres.length == 1
bit = "Genre = #{GenreStore.display(genres[0])}"
else
genres.length > 1
bit = "Genres = #{GenreStore.display(genres[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
languages = searchOptions.languages
if languages? && languages.length > 0
if languages.length == 1
bit = "Language = #{LanguageStore.display(languages[0])}"
else
languages.length > 1
bit = "Languages = #{LanguageStore.display(languages[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
if searchOptions.teaches_beginner || searchOptions.teaches_intermediate || searchOptions.teaches_advanced
bit = "Teaches "
qualifier = ''
if searchOptions.teaches_beginner
qualifier += "Beginner"
if searchOptions.teaches_intermediate
if qualifier.length > 0
qualifier += ", "
qualifier += "Intermediate"
if searchOptions.teaches_advanced
if qualifier.length > 0
qualifier += ", "
qualifier += "Advanced"
if summary.length > 0
summary += ', '
summary += " #{bit}#{qualifier}"
if searchOptions.student_age?
if summary.length > 0
summary += ', '
summary += "Student Age = #{searchOptions.student_age}"
if searchOptions.years_teaching?
if summary.length > 0
summary += ', '
summary += "Years Teaching = #{searchOptions.years_teaching}"
if searchOptions.location?.country?
if summary.length > 0
summary += ', '
summary += "Country = #{searchOptions.location.country}"
if searchOptions.location?.region?
if summary.length > 0
summary += ', '
summary += "Region = #{searchOptions.location.region}"
if summary.length == 0
summary = 'all teachers'
summary
render: () ->
searchDesc = @createSearchDescription()
if @props.teacher
complete = `<span className="search-results-options">
<a href="/client#/teachers/search" className="results-text link">Search Results</a>&nbsp;:&nbsp;
<span className="teacher-name">{this.props.teacher.name}</span>
</span>`
else
complete =
`<span className="search-results-options">
<span className="search-description">
<span className="results-text">Search Results / </span>
<span className="search-summary">{searchDesc}</span>
</span>
</span>`
`<div className="jamclass-search-header">
<a href="/client#/home">JamKazam Home</a>&nbsp;:&nbsp;
<a href="/client#/jamclass">JamClass Home</a>&nbsp;:&nbsp;
<a className="teacher-search-options" href="/client#/jamclass/searchOptions">Teachers Search</a><span
className="teacher-quote"> : </span>
{complete}
</div>`
})

View File

@ -50,6 +50,44 @@ UserStore = context.UserStore
slot.creatorRoleRelative = "your"
slot.mySlot = @mySlot(slot)
processBooking:(booking) ->
booking.neverAccepted = booking.accepter_id?
booking.isCounter = booking.counter_slot? && booking.status != 'canceled' && booking.status != 'suspended'
booking.studentViewing = booking.user_id == context.JK.currentUserId
booking.teacherViewing = !booking.studentViewing
booking.isRequested = booking.status == 'requested' && !booking.isCounter
booking.isCanceled = booking.status == 'canceled'
booking.isSuspended = booking.status == 'suspended'
if booking.isCounter
if booking.counter_slot['is_teacher_created?']
booking.countererId = booking.teacher_id
else
booking.countererId = booking.user_id
selfLastToAct = false
if booking.isRequested
selfLastToAct = booking.studentViewing
else if booking.isCounter
selfLastToAct = booking.countererId == context.JK.currentUserId
else if booking.isCanceled
selfLastToAct = booking.canceler_id == context.JK.currentUserId
else if booking.isSuspended
selfLastToAct = booking.studentViewing
else
selfLastToAct = false
booking.selfLastToAct = selfLastToAct
multipleOptions = false
if booking.neverAccepted
multipleOptions = !(!booking.isCounter && booking.selfLastToAct)
else if booking.isCounter
multipleOptions = !booking.selfLastToAct
else
multipleOptions = false
onlyOption = !multipleOptions
booking.onlyOption = onlyOption
#nextProps.slot_decision = 'counter'
componentWillUpdate: (nextProps, nextState) ->
if nextState.booking?
booking = nextState.booking
@ -61,6 +99,35 @@ UserStore = context.UserStore
@processSlot(booking.default_slot, booking)
@processSlot(booking.alt_slot, booking)
onlyOption:() ->
#(this.props.initial && !this.props.selfLastToAct )|| !(this.props.counter && !this.props.selfLastToAct)
#@initialRequestSlotsVisible() || !@counteredSlotVisible()
#(this.props.initial && this.props.selfLastToAct ) || !(this.props.counter && !this.props.selfLastToAct)
#(@neverAccepted() && @selfLastToAct()) || !(@isCounter() && !@selfLastToAct())
!@multipleOptions()
multipleOptions: () ->
if @neverAccepted()
!(!@isCounter() && @selfLastToAct())
else if this.props.counter
!@selfLastToAct()
else
false
initialRequestSlotsVisible: () ->
console.log("initialRequestSlotsVisible: " + this.neverAccepted() )
#this.neverAccepted() && this.selfLastToAct()
# is there a counter slot showing
counteredSlotVisible: () ->
console.log("isCounter " + this.isCounter() + ", this.selfLastToAct()" + this.selfLastToAct())
this.isCounter() && !this.selfLastToAct()
counterSlotVisible: () ->
true
getInitialState: () ->
{
user: null,
@ -73,14 +140,28 @@ UserStore = context.UserStore
beforeShow: (e) ->
parseId: (id) ->
result = {purpose: null}
bits = id.split('_')
if bits.length == 1
result.id = id
else if bits.length > 1
result.id =bits[0]
result.purpose = bits[1]
else
result.id = id
result
afterShow: (e) ->
@setState({updating: true, counterErrors: null, cancelErrors: null})
parsed = @parseId(e.id)
@setState({updating: true, counterErrors: null, cancelErrors: null, purpose: parsed.purpose})
rest.getLessonBooking({
id: e.id,
id: parsed.id,
}).done((response) => @getLessonBookingDone(response)).fail(@app.ajaxError)
hasFocusedLesson: () ->
this.state.booking.focused_lesson?.id?
@focusedLesson()?
focusedLesson: () ->
this.state?.booking?.focused_lesson
@ -105,6 +186,14 @@ UserStore = context.UserStore
#booking.next_lesson.lesson_booking = booking
@postProcessLesson(booking.next_lesson)
@processBooking(booking)
if booking.onlyOption
# you see two options (accept, and propose new) if it's a counter and you are not the last to act
# the only choice possible in most cases is to propose a new time
#nextState.slot_decision = 'counter'
startSlotDecision = 'counter'
@setState({booking: booking, updating: false, slot_decision: startSlotDecision, updatingLesson: false, update_all: update_all})
getLessonBookingDone: (response) ->
@ -213,7 +302,8 @@ UserStore = context.UserStore
minute = $slot.find('.minute').val()
am_pm = $slot.find('.am_pm').val()
update_all = $slot.find('input.update-all').is(':checked') && @isRecurring()
#update_all = $slot.find('input.update-all').is(':checked') && @isRecurring()
update_all = @state.update_all && @isRecurring()
if hour? and hour != ''
hour = new Number(hour)
@ -227,7 +317,7 @@ UserStore = context.UserStore
else
minute = null
if !@isRecurring()
if !update_all
date = picker.datepicker("getDate")
if date?
date = context.JK.formatDateYYYYMMDD(date)
@ -399,10 +489,10 @@ UserStore = context.UserStore
text = "Preferred day/time for lesson is #{this.slotTime(defaultSlot)}. Secondary option is #{this.slotTime(altSlot)}."
slotTime: (slot, booking = this.state.booking) ->
if @isRecurring(booking)
"#{this.dayOfWeek(slot)} at #{this.dayTime(slot)}"
else
if @hasFocusedLesson() || !@isRecurring(booking)
slot.pretty_start_time
else
"#{this.dayOfWeek(slot)} at #{this.dayTime(slot)}"
slotTimePhrase: (slot) ->
if @isRecurring()
@ -704,9 +794,10 @@ UserStore = context.UserStore
renderStudentRequested: () ->
`<div className="contents">
<div className="row">
<div className="row request-sent">
{this.userHeader(this.myself())}
Your request has been sent. You will receive an email when {this.teacher().name} responds.
{this.createDetail()}
</div>
<LessonBookingDecision {...this.decisionProps([])} />
</div>`
@ -719,6 +810,22 @@ UserStore = context.UserStore
updateCreditCard: (e) ->
window.location.href="/client#/account/paymentHistory"
createDetail: () ->
if @hasFocusedLesson() || !@isRecurring()
if @onlyOption() && @rescheduling()
detail = `<p className="proposing-new-time">You are proposing to change the date/time of the lesson currently scheduled for {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p className="generic-time-stmt">Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}</p>`
else
if @onlyOption() && @rescheduling()
detail = `<p className="proposing-new-time">You are proposing to change the date/time of the lesson currently scheduled for {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p className="generic-time-stmt">Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}</p>`
detail
rescheduling: () ->
@state.purpose == 'rescheduling'
renderStudentComplete: () ->
@renderStudentApproved()
@ -746,10 +853,7 @@ UserStore = context.UserStore
if @studentMadeDefaultSlot()
message = this.slotMessage(this.state.booking.default_slot, 'accept')
if @isRecurring()
detail = `<p>Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p>Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}</p>`
detail = @createDetail()
summary = `<div className="row">
{this.userHeader(this.teacher())}
<p>Has accepted your lesson request.</p>
@ -766,10 +870,7 @@ UserStore = context.UserStore
if @studentMadeDefaultSlot()
message = this.slotMessage(this.state.booking.default_slot, 'accept')
if @isRecurring()
detail = `<p>Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p>Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}</p>`
detail = @createDetail()
summary = `<div className="row">
{this.userHeader(this.teacher())}
<p>Has accepted your lesson request.</p>
@ -780,10 +881,7 @@ UserStore = context.UserStore
if @studentMadeDefaultSlot()
message = this.slotMessage(this.state.booking.default_slot, 'accept')
if @isRecurring()
detail = `<p className="lesson-time">Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p className="lesson-time">Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}</p>`
detail = @createDetail()
summary = `<div className="row">
{this.userHeader(this.teacher())}
@ -847,10 +945,7 @@ UserStore = context.UserStore
if @studentMadeDefaultSlot()
message = this.slotMessage(this.state.booking.default_slot, 'accept')
if @isRecurring()
detail = `<p>Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p>Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}</p>`
detail = @createDetail()
summary = `<div className="row">
{this.userHeader(this.student())}
<p>Is ready to take the lesson.</p>
@ -869,10 +964,7 @@ UserStore = context.UserStore
if @studentMadeDefaultSlot()
message = this.slotMessage(this.state.booking.default_slot, 'accept')
if @isRecurring()
detail = `<p className="lesson-time">Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}</p>`
else
detail = `<p className="lesson-time">Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}</p>`
detail = @createDetail()
summary = `<div className="row">
{this.userHeader(this.teacher())}

View File

@ -57,6 +57,11 @@
@checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}, {selector: 'input.update-all', propsKey: 'update_all'}]
@root = $(@getDOMNode())
@iCheckify()
@slotDate = @root.find('.date-picker')
@slotDate.datepicker({
dateFormat: "D M d yy",
onSelect: ((e) => @toggleDate(e))
})
componentDidUpdate: () ->
@iCheckify()
@ -66,6 +71,14 @@
onSelect: ((e) => @toggleDate(e))
})
toggleDate: (e) ->
componentWillReceiveProps: (nextProps) ->
if @onlyOption()
console.log("setting it counter")
# if this isn't a counter situation, then there is no 'Accept their time', so there should only be one radio button, and we'll select that value already
@setState({"slot-decision": "counter"})
checkboxChanged: (e) ->
@ -126,10 +139,21 @@
nullOp: ()->
onlyOption: () ->
# (!this.props.initial && this.props.selfLastToAct ) || !(this.props.counter && !this.props.selfLastToAct)
!@multipleOptions()
multipleOptions: () ->
if this.props.initial
!(!this.props.counter && this.props.selfLastToAct)
else if this.props.counter
!this.props.selfLastToAct
else
false
render: () ->
showUpdateAll = !this.props.initial
#showUpdateAll = !this.props.initial
if (!this.props.initial && !this.props.counter) || this.props.selfLastToAct
userPromptHeader = `<h3>Would you like to change this lesson?</h3>`
@ -150,7 +174,7 @@
else
verb = "CANCEL"
if this.props.update_all && showUpdateAll
if this.props.update_all
actionBtnText = "#{verb} ALL LESSONS"
else
actionBtnText = "#{verb} LESSON"
@ -159,13 +183,13 @@
else
actionBtnText = "ACCEPT & UPDATE LESSON"
counterClasses={field: true, 'slot-decision-field': true, error: this.props.counterErrors?, counterSelected: this.props.slot_decision == 'counter'}
counterClasses={field: true, 'slot-decision-field': true, error: this.props.counterErrors?, counterSelected: this.props.slot_decision == 'counter', onlyOption: @onlyOption()}
if this.props.counterErrors?
errorText = window.JK.reactErrors(this.props.counterErrors, {day_of_week: 'Day' })
if this.props.is_recurring
if this.props.is_recurring && this.props.update_all
slotAltPrompt = `<div className="slot-alt-prompt">
<span className="alt-date-block">
@ -182,17 +206,18 @@
</span>
</div>`
if showUpdateAll
updateAllField =
`<div className="field update-all-field">
<input disabled={this.props.disabled} className="update-all" type="checkbox" name="update-all" readyOnly="true" onChange={this.nullOp} checked={this.props.update_all} /><label>Update all lessons</label>
</div>`
#if @props.update_all
# updateAllField =
# `<div className="field update-all-field">
# <input disabled={this.props.disabled} className="update-all" type="checkbox" name="update-all" readyOnly="true" onChange={this.nullOp} checked={this.props.update_all} /><label>Update all lessons</label>
# </div>`
updateAllField = null
else
slotAltPrompt = `<div className="slot-alt-prompt">
<span className="alt-date-block">
<span className="alt-date">Date:</span>
<input className="date-picker" type="text" data-slot={i}></input>
<input className="date-picker" name="alt-date-input" type="text" data-slot={i}></input>
</span>
<span className="alt-time-block">
<span className="alt-time">Time:</span>
@ -222,7 +247,7 @@
else
slotDetail = `<div className="slot-detail">{slot.slotTime}</div>`
slots.push(`<div key={slot.id} className="field slot-decision-field">
slots.push(`<div key={slot.id} data-slot-id={slot.id} className="field slot-decision-field">
<div className="label-area">
<input disabled={this.props.disabled} className="slot-decision" type="radio" name="slot-decision"
value={slot.id} readyOnly={true} defaultChecked={slot.id == this.props.slot_decision} /><label>{this.slotLabelText(i, slot)}</label>
@ -234,6 +259,12 @@
# if you have issued a counter, you should be able to withdraw it
# TODO
#cancelField = `<div className="field slot-decision-field">
# <input disabled={this.props.disabled} className="slot-decision" type="radio" name="slot-decision" value="decline" readyOnly={true} defaultChecked={this.props.slot_decision == 'decline'} /><label>{declineVerb} lesson
# request</label>
#</div>`
cancelField = null
`<div className="row">
<div className="column column-left">
{userPromptHeader}
@ -247,10 +278,7 @@
</div>
{slotAltPrompt}
</div>
<div className="field slot-decision-field">
<input disabled={this.props.disabled} className="slot-decision" type="radio" name="slot-decision" value="decline" readyOnly={true} defaultChecked={this.props.slot_decision == 'decline'} /><label>{declineVerb} lesson
request</label>
</div>
{cancelField}
{updateAllField}
</div>
</div>

View File

@ -294,18 +294,24 @@ UserStore = context.UserStore
logger.debug("testDriveCount: " + testDriveCount)
testDriveCountInt = parseInt(testDriveCount);
if context._.isNaN(testDriveCountInt)
testDriveCountInt = 3
context.JK.GA.trackTestDrivePurchase(testDriveCountInt);
if response.test_drive?.teacher_id
teacher_id = response.test_drive.teacher_id
if testDriveCount == 1
text = "You have purchased a TestDrive credit and have used it to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
if testDriveCount == '1'
text = "You have purchased 1 TestDrive credit and have used it to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
else
text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit it to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
location = "/client#/jamclass"
else
if @state.teacher?.id
# the user bought the testdrive, and there is a teacher of interest in context (but no booking)
if testDriveCount == 1
if testDriveCount == '1'
text = "You now have 1 TestDrive credit.<br/><br/>We've taken you to the lesson booking screen for the teacher you initially showed interest in."
location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
else
@ -313,7 +319,7 @@ UserStore = context.UserStore
location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
else
# the user bought test drive, but 'cold' , i.e., no teacher in context
if testDriveCount == 1
if testDriveCount == '1'
text = "You now have 1 TestDrive credit.<br/><br/>We've taken you to the Teacher Search screen, so you can search for teachers right for you."
location = "/client#/teachers/search"
else

View File

@ -0,0 +1,49 @@
context = window
@MusicNotationUploadDialog = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(AttachmentStore, "onAttachmentStore")]
getInitialState: () ->
{
uploading: false
}
onAppInit: (@app) ->
onAttachmentStore: (attachmentState) ->
@setState(attachmentState)
handleCloseMessage: (e) ->
e.preventDefault()
@app.layout.closeDialog('music-notation-upload-dialog')
render: () ->
if @state.uploading
state =
`<div>
<h3>Please wait while we upload your attachment to the lesson...</h3>
<div className="spinner spinner-large"></div>
</div>`
else
state =
`<div>
<h3>Your file has been uploaded.</h3>
</div>`
`<div className="MusicNotationUploadDialog">
<div className="content-head">
<img className="content-icon" src="/assets/content/icon_add.png" height={19} width={19}/>
<h1>Uploading Attachment</h1>
</div>
<div className="dialog-inner">
{state}
<div className="actions">
<a className="button-orange " onClick={this.handleCloseMessage}>CLOSE</a>
</div>
</div>
</div>`
})

View File

@ -704,14 +704,21 @@ proficiencyDescriptionMap = {
render: () ->
if @state.user?
avatar = context.JK.resolveAvatarUrl(@state.user.photo_url);
if @state.user?.teacher?
mainContent = @mainContent()
profileLeft = @profileLeft()
else
if context.JK.currentUserId? && @state.user?.id == context.JK.currentUserId
noTeacherProfile = `<div className="no-teacher-profile">You have no teacher profile defined yet. <a onClick={this.editTeacherProfile}>Start making one now.</a></div>`
if @state.user?
if @state.user.teacher?
mainContent = @mainContent()
profileLeft = @profileLeft()
else
noTeacherProfile = `<div className="no-teacher-profile">This user has no teacher profile.</div>`
if context.JK.currentUserId? && @state.user?.id == context.JK.currentUserId
noTeacherProfile = `<div className="no-teacher-profile">You have no teacher profile defined yet. <a onClick={this.editTeacherProfile}>Start making one now.</a></div>`
else
noTeacherProfile = `<div className="no-teacher-profile">This user has no teacher profile.</div>`
mainContent = `<div className="">
{noTeacherProfile}
</div>`
else
noTeacherProfile = `<div><div className="loading-profile">Loading profile...</div><div className="spinner spinner-large"></div></div>`
mainContent = `<div className="">
{noTeacherProfile}
</div>`
@ -730,10 +737,7 @@ proficiencyDescriptionMap = {
</div>`
`<div className="content-body-scroller">
<div className="profile-header profile-head">
<div className="user-header">
<h2 id="username"></h2>
</div>
<JamClassSearchHeader teacher={this.state.user}/>
{actionButtons}
<br clear="all"/><br />

View File

@ -5,11 +5,25 @@ LocationActions = @LocationActions
@TeacherSearchOptionsScreen = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged"),
mixins: [ICheckMixin, Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged"),
Reflux.listenTo(@TeacherSearchStore, "onTeacherSearchChanged")]
LIMIT: 20
componentDidMount: () ->
@checkboxes = [
{selector: 'input.onlyMySchool', stateKey: 'onlyMySchool'},
{selector: 'input.beginner-level', stateKey: 'beginner-level'},
{selector: 'input.intermediate-level', stateKey: 'intermediate-level'},
{selector: 'input.advanced-level', stateKey: 'advanced-level'}]
@root = $(@getDOMNode())
@iCheckify()
componentDidUpdate: () ->
@iCheckify()
getInitialState: () ->
{options: {onlyMySchool: true}}
@ -71,6 +85,20 @@ LocationActions = @LocationActions
options.onlyMySchool = checked
@setState(options)
checkboxChanged: (e) ->
$target = $(e.target)
if $target.is('.onlyMySchool')
state = {}
state['onlyMySchool'] = $target.is(':checked')
@setState(state)
@onlyMySchoolChange(e)
else
state = {}
state[$target.attr('name')] = $target.is(':checked')
@setState(state)
@levelChanged(e)
render: () ->
if @state.user?.school_id?
onlySchoolOption =
@ -114,15 +142,15 @@ LocationActions = @LocationActions
<h3>Student Levels Taught:</h3>
<div className="teaching-level beginner-level">
<input name="beginner-level" type="checkbox" data-purpose="teaches_beginner" onChange={this.levelChanged} checked={this.state.options.teaches_beginner}></input>
<input className="teachlevel beginner-level" name="beginner-level" type="checkbox" data-purpose="teaches_beginner" onChange={this.levelChanged} checked={this.state.options.teaches_beginner}></input>
<label htmlFor="beginner-level">Beginner</label>
</div>
<div className="teaching-level intermediate-level">
<input name="intermediate-level" type="checkbox" data-purpose="teaches_intermediate" onChange={this.levelChanged} checked={this.state.options.teaches_intermediate}></input>
<input className="teachlevel intermediate-level" name="intermediate-level" type="checkbox" data-purpose="teaches_intermediate" onChange={this.levelChanged} checked={this.state.options.teaches_intermediate}></input>
<label htmlFor="intermediate-level">Intermediate</label>
</div>
<div className="teaching-level advanced-level">
<input name="advanced-level" type="checkbox" data-purpose="teaches_advanced" onChange={this.levelChanged} checked={this.state.options.teaches_advanced}></input>
<input className="teachlevel advanced-level" name="advanced-level" type="checkbox" data-purpose="teaches_advanced" onChange={this.levelChanged} checked={this.state.options.teaches_advanced}></input>
<label htmlFor="advanced-level">Advanced</label>
</div>
</div>

View File

@ -28,7 +28,7 @@ ProfileActions = @ProfileActions
refreshing: false
getInitialState: () ->
{searchOptions: {}, results: [], user: null}
{searchOptions: {}, results: [], user: null, searching: false}
onAppInit: (@app) ->
@app.bindScreen('teachers/search', {beforeShow: @beforeShow, afterShow: @afterShow, afterHide: @afterHide})
@ -58,7 +58,7 @@ ProfileActions = @ProfileActions
@needToSearch = true
onTeacherSearchResultsStore: (results) ->
results.searching = false
#results.searching = false
@refreshing = false
@contentBodyScroller.find('.infinite-scroll-loader-2').remove()
@setState(results)
@ -89,7 +89,7 @@ ProfileActions = @ProfileActions
if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight
$scroller.append('<div class="infinite-scroll-loader-2">... Loading more Teachers ...</div>')
@refreshing = true
@setState({searching: true})
#@setState({searching: true})
logger.debug("refreshing more teachers for infinite scroll")
TeacherSearchResultsActions.nextPage()
)
@ -149,7 +149,9 @@ ProfileActions = @ProfileActions
moreAboutTeacher: (user, e) ->
e.preventDefault()
ProfileActions.viewTeacherProfile(user, '/client#/teachers/search', 'BACK TO TEACHER SEARCH')
context.location = "/client#/profile/teacher/#{user.id}"
#ProfileActions.viewTeacherProfile(user, '/client#/teachers/search', 'BACK TO TEACHER SEARCH')
bookTestDrive: (user, e) ->
e.preventDefault()
@ -210,162 +212,60 @@ ProfileActions = @ProfileActions
target.trigger( 'destroy.dot' );
teacherBio.css('height', 'auto')
createSearchDescription: () ->
searchOptions = TeacherSearchStore.getState()
summary = ''
if searchOptions.onlyMySchool
summary += "From My School Only"
instruments = searchOptions.instruments
if instruments? && instruments.length > 0
if instruments.length == 1
bit = "Instrument = #{InstrumentStore.display(instruments[0])}"
else
instruments.length > 1
bit = "Instruments = #{InstrumentStore.display(instruments[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
subjects = searchOptions.subjects
if subjects? && subjects.length > 0
if subjects.length == 1
bit = "Subject = #{SubjectStore.display(subjects[0])}"
else
subjects.length > 1
bit = "Subjects = #{SubjectStore.display(subjects[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
genres = searchOptions.genres
if genres? && genres.length > 0
if genres.length == 1
bit = "Genre = #{GenreStore.display(genres[0])}"
else
genres.length > 1
bit = "Genres = #{GenreStore.display(genres[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
languages = searchOptions.languages
if languages? && languages.length > 0
if languages.length == 1
bit = "Language = #{LanguageStore.display(languages[0])}"
else
languages.length > 1
bit = "Languages = #{LanguageStore.display(languages[0])} ... "
if summary.length > 0
summary += ', '
summary += " #{bit}"
if searchOptions.teaches_beginner || searchOptions.teaches_intermediate || searchOptions.teaches_advanced
bit = "Teaches "
qualifier = ''
if searchOptions.teaches_beginner
qualifier += "Beginner"
if searchOptions.teaches_intermediate
if qualifier.length > 0
qualifier += ", "
qualifier += "Intermediate"
if searchOptions.teaches_advanced
if qualifier.length > 0
qualifier += ", "
qualifier += "Advanced"
if summary.length > 0
summary += ', '
summary += " #{bit}#{qualifier}"
if searchOptions.student_age?
if summary.length > 0
summary += ', '
summary += "Student Age = #{searchOptions.student_age}"
if searchOptions.years_teaching?
if summary.length > 0
summary += ', '
summary += "Years Teaching = #{searchOptions.years_teaching}"
if searchOptions.location?.country?
if summary.length > 0
summary += ', '
summary += "Country = #{searchOptions.location.country}"
if searchOptions.location?.region?
if summary.length > 0
summary += ', '
summary += "Region = #{searchOptions.location.region}"
if summary.length == 0
summary = 'all teachers'
summary
render: () ->
searchDesc = @createSearchDescription()
resultsJsx = []
for user in @state.results
if @state.currentPage == 1 && @state.searching
resultsJsx = `<div className="spinner spinner-large"></div>`
else
for user in @state.results
photo_url = user.photo_url
if !photo_url?
photo_url = '/assets/shared/avatar_generic.png'
photo_url = user.photo_url
if !photo_url?
photo_url = '/assets/shared/avatar_generic.png'
bio = user.teacher.biography
if !bio?
bio = 'No bio'
bio = user.teacher.biography
if !bio?
bio = 'No bio'
school_on_school = user.teacher.school_id? && @state.user?.school_id? && user.teacher.school_id == @state.user.school_id
school_on_school = user.teacher.school_id? && @state.user?.school_id? && user.teacher.school_id == @state.user.school_id
bookSingleBtn = null
bookTestDriveBtn = null
bookSingleBtn = null
bookTestDriveBtn = null
if !school_on_school && (!@state.user? || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?'])
bookTestDriveBtn = `<a className="button-orange try-test-drive" onClick={this.bookTestDrive.bind(this, user)}>BOOK TESTDRIVE LESSON</a>`
else
bookSingleBtn = `<a className="button-orange try-normal" onClick={this.bookNormalLesson.bind(this, user)}>BOOK LESSON</a>`
resultsJsx.push(`<div key={user.id} className="teacher-search-result" data-teacher-id={user.id}>
<div className="user-avatar">
<div className="avatar small">
<img src={photo_url} />
if !school_on_school && (!@state.user? || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?'])
bookTestDriveBtn = `<a className="button-orange try-test-drive" onClick={this.bookTestDrive.bind(this, user)}>BOOK TESTDRIVE LESSON</a>`
else
bookSingleBtn = `<a className="button-orange try-normal" onClick={this.bookNormalLesson.bind(this, user)}>BOOK LESSON</a>`
resultsJsx.push(`<div key={user.id} className="teacher-search-result" data-teacher-id={user.id}>
<div className="user-avatar">
<div className="avatar small">
<img src={photo_url} />
</div>
<div className="user-name">
{user.name}
</div>
</div>
<div className="user-name">
{user.name}
<div className="user-info">
<div className="teacher-bio">
{bio}
<a className="readmore" onClick={this.readMore}>more</a>
</div>
<div className="teacher-actions">
<a className="button-orange more-about-teacher" onClick={this.moreAboutTeacher.bind(this, user)}>MORE ABOUT THIS TEACHER</a>
{bookTestDriveBtn}
{bookSingleBtn}
</div>
</div>
</div>
<div className="user-info">
<div className="teacher-bio">
{bio}
<a className="readmore" onClick={this.readMore}>more</a>
</div>
<div className="teacher-actions">
<a className="button-orange more-about-teacher" onClick={this.moreAboutTeacher.bind(this, user)}>MORE ABOUT THIS TEACHER</a>
{bookTestDriveBtn}
{bookSingleBtn}
</div>
</div>
<br className="clearall" />
</div>`)
<br className="clearall" />
</div>`)
`<div className="content-body-scroller">
<div className="screen-content">
<div className="header">
<a href="/client#/home">JamKazam Home</a>&nbsp;:&nbsp;
<a href="/client#/jamclass">JamClass Home</a>&nbsp;:&nbsp;
<a className="teacher-search-options" href="/client#/jamclass/searchOptions" >Teachers Search</a><span className="teacher-quote"> :</span>
<span className="search-results-options">
<span className="search-description">
<span className="results-text">Search Results / </span>
<span className="search-summary">{searchDesc}</span>
</span>
</span>
<JamClassSearchHeader/>
</div>
<div className="results">
{resultsJsx}

View File

@ -5,4 +5,5 @@ context = window
startAttachNotation: {}
startAttachAudio: {}
uploadNotations: {}
uploadAudios: {}
})

View File

@ -45,8 +45,8 @@ rest = context.JK.Rest()
</li>
</ol>
<p>While you're getting this done, if you want to learn more about all the nifty features you can access in
JamClass and in JamKazam in general, you can check out our online <a href="" target="_blank"
onClick={alert.bind('not yet')}>JamClass
JamClass and in JamKazam in general, you can check out our online <a target="_blank"
href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles">JamClass
User Guide</a>.</p>
</div>`
`<div className="top-container">

View File

@ -40,14 +40,18 @@ teacherActions = window.JK.Actions.Teacher
enableICheck: (e) ->
if !@root?
return
checkBoxes = @root.find('input[type="checkbox"]')
if checkBoxes.length > 0
context.JK.checkbox(checkBoxes)
checkBoxes.on('ifChanged', (e) => @checkIfCanFire(e))
radioBoxes = @root.find('input[type="radio"]')
if radioBoxes.length > 0
context.JK.checkbox(radioBoxes)
radioBoxes.on('ifChanged', (e) => @checkIfCanFire(e))
for checkbox in @checkboxes
selector = checkbox.selector
checkBoxes = @root.find(selector + '[type="checkbox"]')
if checkBoxes.length > 0
context.JK.checkbox(checkBoxes)
checkBoxes.on('ifChanged', (e) => @checkIfCanFire(e))
radioBoxes = @root.find(selector + '[type="radio"]')
if radioBoxes.length > 0
context.JK.checkbox(radioBoxes)
radioBoxes.on('ifChanged', (e) => @checkIfCanFire(e))
true

View File

@ -23,7 +23,7 @@ teacherActions = window.JK.Actions.Teacher
lesson.isAdmin = context.JK.currentUserAdmin
lesson.schoolOnSchool = lesson['school_on_school?']
lesson.cardNotOk = !lesson.schoolOnSchool && !lesson.lesson_booking.card_presumed_ok
lesson.isActive = lesson['is_active?']
if (lesson.status == 'requested' || lesson.status == 'countered')
lesson.isRequested = true
if lesson.cardNotOk

View File

@ -105,6 +105,8 @@ AttachmentActions = @AttachmentActions
formData.append('lesson_session_id', @lessonId);
formData.append('attachment_type', 'notation')
@app.layout.showDialog('music-notation-upload-dialog')
rest.uploadMusicNotations(formData)
.done((response) => @doneUploadingNotatations(notations, response, doneCallback, failCallback))
.fail((jqXHR) => @failUploadingNotations(jqXHR, failCallback))
@ -137,6 +139,71 @@ AttachmentActions = @AttachmentActions
else
@app.notifyServerError(jqXHR, "Unable to upload music notations");
onUploadAudios: (notations, doneCallback, failCallback) ->
logger.debug("beginning upload of audio", notations)
@uploading = true
@changed()
formData = new FormData()
maxExceeded = false;
$.each(notations, (i, file) => (
max = 10 * 1024 * 1024;
if file.size > max
maxExceeded = true
return false
formData.append('files[]', file)
))
if maxExceeded
@app.notify({
title: "Maximum Music Audio Size Exceeded",
text: "You can only upload files up to 10 megabytes in size."
})
failCallback()
@uploading = false
@changed()
return
formData.append('lesson_session_id', @lessonId);
formData.append('attachment_type', 'audio')
@app.layout.showDialog('music-notation-upload-dialog')
rest.uploadMusicNotations(formData)
.done((response) => @doneUploadingAudios(notations, response, doneCallback, failCallback))
.fail((jqXHR) => @failUploadingAudios(jqXHR, failCallback))
doneUploadingAudios: (notations, response, doneCallback, failCallback) ->
@uploading = false
@changed()
error_files = [];
$.each(response, (i, music_notation) => (
if music_notation.errors
error_files.push(notations[i].name)
)
)
if error_files.length > 0
failCallback()
@app.notifyAlert("Failed to upload audio files.", error_files.join(', '));
else
doneCallback()
failUploadingAudios: (jqXHR, failCallback) ->
@uploading = false
@changed()
if jqXHR.status == 413
# the file is too big. Let the user know.
# This should happen when they select the file, but a misconfiguration on the server could cause this.
@app.notify({
title: "Maximum Music Audio Size Exceeded",
text: "You can only upload files up to 10 megabytes in size."
})
else
@app.notifyServerError(jqXHR, "Unable to upload music audio files");
changed: () ->
this.trigger({lessonId: @lessonId, uploading: @uploading})
}

View File

@ -11,6 +11,7 @@ TeacherSearchResultsActions = @TeacherSearchResultsActions
results: []
page: 1
limit: 20
searching: false
init: ->
# Register with the app store to get @app
@ -22,6 +23,7 @@ TeacherSearchResultsActions = @TeacherSearchResultsActions
onReset: () ->
@results = []
@page = 1
@searching = true
@changed()
query = @createQuery()
@ -29,11 +31,13 @@ TeacherSearchResultsActions = @TeacherSearchResultsActions
rest.searchTeachers(query)
.done((response) =>
@next = response.next
@searching = false
@results.push.apply(@results, response.entries)
@changed()
)
.fail((jqXHR, textStatus, errorMessage) =>
@searching = false
@changed()
@app.ajaxError(jqXHR, textStatus, errorMessage)
)
@ -42,18 +46,22 @@ TeacherSearchResultsActions = @TeacherSearchResultsActions
query = @createQuery()
@searching = true
rest.searchTeachers(query)
.done((response) =>
@next = response.next
@results.push.apply(@results, response.entries)
@searching = false
@changed()
)
.fail((jqXHR, textStatus, errorMessage) =>
@searching = false
@app.ajaxError(jqXHR, textStatus, errorMessage)
@changed()
)
getState: () ->
({results: @results, next: @next, currentPage: @page})
({results: @results, next: @next, currentPage: @page, searching: @searching})
changed:() ->
@trigger(@getState())

View File

@ -76,7 +76,8 @@
$('.like-link').click(function() {
var like_site = $(this).data('site');
JK.GA.trackJKSocial(JK.GA.Categories.jkLike, like_site, JK.clientType());
// removed because we are juggling 20 events max in GA
//JK.GA.trackJKSocial(JK.GA.Categories.jkLike, like_site, JK.clientType());
window.open("/endorse/0/"+like_site, '_blank');
});
}

View File

@ -190,7 +190,7 @@
logger.debug("marking all unassigned inputs length=(" + $allInputs.length + ")")
var maxTries = 20;
var maxTries = 12; // we only allow up to 6 tracks, 12 inputs
for(var i = 0; i < maxTries; i++) {
$unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)');
@ -1105,6 +1105,11 @@
return false;
}
if (!passedOnce) {
passedOnce = true;
autoAssignToSingleInput();
}
if(!savedProfile) {
context.jamClient.FTUESetMusicProfileName(gearUtils.createProfileName(selectedDeviceInfo));
var result = context.jamClient.FTUESave(true);
@ -1122,10 +1127,6 @@
}
}
if (!passedOnce) {
passedOnce = true;
autoAssignToSingleInput();
}
// keep the shared state between step 2 and step 3 up-to-date
wizard.setChosenInputs(context._.map($assignedInputs, function(input) { return $(input).attr('data-id') }));
@ -1147,12 +1148,12 @@
}
var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)')
$allInputs.eq(0).iCheck('check').attr('checked', 'checked')
var firstInputDomNode = $allInputs.get(0)
var maxTries = 20;
var maxTries = 36;
for(var i = 0; i < maxTries; i++) {
var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked')
var $allInputs = $inputChannels.find('input[type="checkbox"]')
var firstInputDomNode = $allInputs.get(0)
if ($assignedInputs.length == 1) {
break;
}
@ -1169,9 +1170,7 @@
}
})
}
}
}
function onFocus() {
@ -1182,6 +1181,7 @@
}
function newSession() {
passedOnce = false;
savedProfile = false;
initialScan = false;
deviceInformation = gearUtils.loadDeviceInfo();

View File

@ -79,6 +79,15 @@
}
}
.onlyOption {
input[type="radio"], .iradio_minimal {
display:none;
}
.slot-alt-prompt {
padding-left:0;
}
}
.slot-detail {
color: $ColorTextTypical;
display: inline-block;
@ -160,6 +169,14 @@
.user-name {
line-height:48px;
vertical-align:middle;
color:white;
}
.request-sent {
color:$ColorTextTypical;
.generic-time-stmt, .proposing-new-time {
margin-top:10px !important;
}
}
.avatar {
position:absolute;

View File

@ -194,7 +194,22 @@
.years {float:right}
}
.profileNavActions {
margin-right: -3px;
right:20px;
top:10px;
position:absolute;
}
.spinner-large {
width:200px;
height:200px;
position:relative;
margin:0 auto;
}
.loading-profile {
text-align:center;
color:white;
}
.ratings-block {

View File

@ -1,5 +1,41 @@
@import "client/common";
.jamclass-search-header {
a {
font-size:16px;
text-decoration:underline;
margin-bottom:5px;
}
.search-results-options {
font-size:16px;
color:$ColorTextTypical;
}
.search-summary {
font-size:11px;
}
.search-description {
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align:middle;
}
.search-summary {
line-height:16px;
vertical-align: middle;
}
.results-text {
&.link {
}
}
}
#teacherSearch {
div[data-react-class="TeacherSearchScreen"] {
height:100%;
@ -9,29 +45,21 @@
padding:20px;
}
.header {
.jamclass-search-header {
margin-bottom:10px;
}
a {
font-size:16px;
text-decoration:underline;
margin-bottom:5px;
}
.search-results-options {
font-size:16px;
color:$ColorTextTypical;
}
.spinner-large {
width:200px;
height:200px;
position:relative;
margin:0 auto;
}
a.readmore {
display:none;
}
.search-summary {
font-size:11px;
}
.teacher-search-result {
@include border_box_sizing;
clear:both;
@ -108,17 +136,7 @@
padding-right: 31px;
margin-bottom: 20px;
}
.search-description {
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align:middle;
}
.search-summary {
line-height:16px;
vertical-align: middle;
}
.result-text {
line-height:16px;
}

View File

@ -9,6 +9,29 @@
.checkbox-scroller {
height:100px;
margin-top:0;
.checkItem {
clear: both;
margin-bottom:4px;
label {
color: black;
display: inline;
float: left;
font-size: 1em;
}
.icheckbox_minimal {
color: black;
display: inline;
float: left;
font-size: 1em;
margin-right:5px;
}
input {
width: auto;
text-align: left;
float: left;
display: inline;
}
}
}
min-width:200px;
width:25%;
@ -72,6 +95,10 @@
}
.student-levels-taught {
.icheckbox_minimal {
top:3px;
margin-right:5px;
}
.teaching-level {
margin-bottom:10px;
}

View File

@ -0,0 +1,35 @@
@import "client/common";
#music-notation-upload-dialog {
width: 500px;
max-height:500px;
h3 {
color:white;
margin-bottom:20px;
}
.dialog-inner {
width: auto;
height:100%;
@include border_box_sizing;
margin-top: -29px;
padding: 50px 25px 25px;
}
div[data-react-class="MusicNotationUploadDialog"] {
}
.MusicNotationUploadDialog {
height:100%;
}
.spinner-large {
width:200px;
height:200px;
line-height: 200px;
position:relative;
margin:25px auto;
}
.actions {
text-align:right;
}
}

View File

@ -16,6 +16,11 @@
overflow-y:auto;
}
.title-artist {
margin-left:25px;
}
.action-buttons {
margin-bottom:10px;
}

View File

@ -13,7 +13,6 @@
height: 36px;
}
.recording-controls {
margin-top: 15px;
padding: 3px 5px 3px 10px;

View File

@ -52,7 +52,7 @@ class ApiLessonBookingsController < ApiController
specified_slot.timezone = params[:timezone]
slots << specified_slot
end
@lesson_booking = LessonBooking.book_free(current_user, teacher, slots, params[:description])
if @lesson_booking.errors.any?
@ -129,10 +129,10 @@ class ApiLessonBookingsController < ApiController
def accept
next_lesson = @lesson_booking.next_lesson
result = next_lesson.accept({
message: params[:message],
slot: params[:slot],
accepter: current_user
})
message: params[:message],
slot: params[:slot],
accepter: current_user
})
if result.errors.any?
if result.is_a?(JamRuby::LessonBooking)
@ -159,16 +159,20 @@ class ApiLessonBookingsController < ApiController
slot = LessonBookingSlot.new
if @lesson_booking.recurring
slot.slot_type = LessonBookingSlot::SLOT_TYPE_RECURRING
slot.day_of_week = params[:day_of_week]
if params[:update_all]
slot.slot_type = LessonBookingSlot::SLOT_TYPE_RECURRING
else
slot.slot_type = LessonBookingSlot::SLOT_TYPE_SINGLE
end
else
slot.slot_type = LessonBookingSlot::SLOT_TYPE_SINGLE
end
if params[:date].present?
day = params[:date]
day = Date.parse(day) if day && !day.include?('NaN')
slot.preferred_day = day
end
slot.day_of_week = params[:day_of_week]
if params[:date].present?
day = params[:date]
day = Date.parse(day) if day && !day.include?('NaN')
slot.preferred_day = day
end
slot.hour = params[:hour]
slot.minute = params[:minute]
@ -176,10 +180,10 @@ class ApiLessonBookingsController < ApiController
slot.update_all = params[:update_all]
result = target_lesson.counter({
proposer: current_user,
message: params[:message],
slot: slot
})
proposer: current_user,
message: params[:message],
slot: slot
})
if result.errors.any?
if result.is_a?(JamRuby::LessonBooking)
@ -188,7 +192,7 @@ class ApiLessonBookingsController < ApiController
recursive_errors(result, [:lesson_booking_slots])
else
raise "unknown response type in counter #{result.class}"
end
end
return
end
@lesson_booking.reload
@ -203,10 +207,10 @@ class ApiLessonBookingsController < ApiController
@lesson_session = target_lesson
result = target_lesson.cancel({
canceler: current_user,
message: params[:message],
update_all: true
})
canceler: current_user,
message: params[:message],
update_all: params[:update_all]
})
if result.errors.any?
if result.is_a?(JamRuby::LessonBooking)

View File

@ -94,7 +94,7 @@ 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
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
@ -135,6 +135,7 @@ class ApiLessonSessionsController < ApiController
end
msg = ChatMessage.create(me, nil, '', ChatMessage::CHANNEL_LESSON, nil, other, @lesson_session, 'JamKazam Recording', nil, claimed_recording)
UserMailer.lesson_attachment(me, other, @lesson_session, claimed_recording)
end
render :json => {}, :status => 200

View File

@ -40,6 +40,8 @@ class ApiMusicNotationsController < ApiController
end
msg = ChatMessage.create(me, nil, '', ChatMessage::CHANNEL_LESSON, nil, other, lesson_session, purpose, music_notation)
UserMailer.lesson_attachment(me, other, lesson_session, music_notation)
end
end

View File

@ -955,10 +955,13 @@ class ApiUsersController < ApiController
end
def lookup_user
User.includes([{musician_instruments: :instrument},
{band_musicians: :user},
{genre_players: :genre},
:bands, :instruments, :genres, :jam_track_rights, :affiliate_partner])
:bands, :instruments, :genres, :jam_track_rights,
:affiliate_partner, :reviews, :review_summary, :recordings,
:teacher => [:subjects, :instruments, :languages, :genres, :teachers_languages, :experiences_teaching, :experiences_award, :experiences_education, :reviews, :review_summary]])
.find(params[:id])
end

View File

@ -380,7 +380,8 @@ class UsersController < ApplicationController
js =<<JS
<script type="text/javascript">
$(function() {
JK.GA.trackJKSocial(JK.GA.Categories.jkLike, '#{service}', 'email');
// // removed because we are juggling 20 events max in GA
// JK.GA.trackJKSocial(JK.GA.Categories.jkLike, '#{service}', 'email');
window.location = "#{url}";
});
</script>

View File

@ -77,4 +77,8 @@ module MusicSessionHelper
def pretty_scheduled_start(music_session, with_timezone, shorter = false)
music_session.pretty_scheduled_start(with_timezone, shorter)
end
def pretty_scheduled_start_slot(slot, with_timezone)
slot.pretty_scheduled_start(with_timezone)
end
end

View File

@ -3,7 +3,8 @@ object @lesson_session
attributes :id, :lesson_booking_id, :lesson_type, :duration, :price, :teacher_complete, :student_complete,
:status, :student_canceled, :teacher_canceled, :student_canceled_at, :teacher_canceled_at, :student_canceled_reason,
:teacher_canceled_reason, :status, :success, :teacher_unread_messages, :student_unread_messages, :is_active?, :recurring,
:analysed, :school_on_school?, :teacher_id, :student_id, :pretty_scheduled_start, :scheduled_start, :teacher_short_canceled
:analysed, :school_on_school?, :teacher_id, :student_id, :pretty_scheduled_start, :scheduled_start, :teacher_short_canceled,
:best_display_time
node do |lesson_session|
{
@ -17,6 +18,15 @@ child(:lesson_booking => :lesson_booking) {
}
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?, :timezone, :pretty_timezone
node :pretty_scheduled_start_with_timezone do |slot|
pretty_scheduled_start_slot(slot, true)
end
}
child(:music_session => :music_session) {
attributes :id, :music_session_id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat,
:band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :play_count, :scheduled_duration,

View File

@ -3,7 +3,7 @@ node :next do |page|
end
node :entries do |page|
partial "api_users/show", object: @users
partial "api_users/show_teacher_index", object: @users
end
node :total_entries do |page|

View File

@ -0,0 +1,8 @@
object @user
attributes :id, :first_name, :last_name, :name, :photo_url
child :teacher do |teacher|
attributes :id, :biography, :school_id
end

View File

@ -14,25 +14,7 @@ script type='text/template' id='template-lesson-session-actions'
li data-lesson-option="attach-audio"
a href='#' Attach Audio File
= '{% } else if (data.isRequested) { %}'
ul
li data-lesson-option="status"
a href='#' View Status
li data-lesson-option="messages"
a href='#' View Messages
li data-lesson-option="messages"
a href='#' Attach Message
li data-lesson-option="cancel"
= '{% if (data.isStudent) { %}'
a href='#' Cancel Request
= '{% } else { %}'
a href='#' Decline Request
= '{% } %}'
= '{% } else if (data.isScheduled) { %}'
= '{% } else if (data.isActive) { %}'
ul
li data-lesson-option="status"
a href='#' View Status
@ -67,6 +49,23 @@ script type='text/template' id='template-lesson-session-actions'
li data-lesson-option="start-65-ago"
a href='#' Set Start 65 Min Ago
= '{% } %}'
= '{% } else if (data.isRequested) { %}'
ul
li data-lesson-option="status"
a href='#' View Status
li data-lesson-option="messages"
a href='#' View Messages
li data-lesson-option="messages"
a href='#' Attach Message
li data-lesson-option="cancel"
= '{% if (data.isStudent) { %}'
a href='#' Cancel Request
= '{% } else { %}'
a href='#' Decline Request
= '{% } %}'
= '{% } else { %}'
ul

View File

@ -16,14 +16,13 @@
.wizard-step{ 'layout-wizard-step' => "1", 'dialog-title' => "Select & Test Audio Gear", 'dialog-purpose' => "SelectAudioGear" }
.ftuesteps
.clearall
.help-text In this step, you will select, configure, and test your audio gear. Please watch the video for best instructions.
.help-text In this step, you will select, configure, and test your audio gear. Please click Instructions button for guidance.
.wizard-step-content
.wizard-step-column
%h2 Instructions
.ftue-box.instructions
%ul
%li Select audio interface for inputs and outputs.
%li Check input ports to which you will connect instruments or mics.
%li Check output ports you will use to monitor.
%li Configure interface settings.
%li View test results.

View File

@ -54,3 +54,4 @@
= render 'dialogs/cancelLessonDialog'
= render 'dialogs/rescheduleLessonDialog'
= render 'dialogs/rateUserDialog'
= render 'dialogs/musicNotationUploadDialog'

View File

@ -0,0 +1,2 @@
.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='music-notation-upload-dialog' id='music-notation-upload-dialog'
= react_component 'MusicNotationUploadDialog', {}

View File

@ -8,7 +8,7 @@
%a{:hoveraction => "{{data.feed_item.helpers.artist_hoveraction}}", :profileaction => "{{data.feed_item.helpers.artist_hoveraction}}", :"{{data.feed_item.helpers.artist_datakey}}" => "{{data.feed_item.helpers.artist_id}}"}
%img{ src: '{{data.feed_item.helpers.avatar}}' }
/ type and artist
.left.ml20.w15
.left.ml20.w15.title-artist
.title
%a.title-text{:href => "/recordings/{{data.candidate_claimed_recording.id}}", :rel => "external", :hoveraction => "recording", :'recording-id' => '{{data.candidate_claimed_recording.id}}'} RECORDING
%a.edit-recording-dialog{href: "#"} (edit)

View File

@ -1,7 +1,11 @@
require 'factory_girl'
require 'timecop'
require 'rspec-rails'
begin
require Rails.root.join('spec', 'support', 'lessons.rb')
rescue LoadError
puts "for production; we ignore LoadError"
end
namespace :lessons do
@ -77,8 +81,8 @@ namespace :lessons do
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'])
user = User.find_by_email(ENV['STUDENT'])
teacher = User.find_by_email(ENV['TEACHER'])
slots = []
@ -91,7 +95,6 @@ namespace :lessons do
user.save!
end
booking = LessonBooking.book_test_drive(user, teacher, slots, "Hey I've heard of you before.")
if booking.errors.any?
puts booking.errors.inspect
@ -100,7 +103,7 @@ namespace :lessons do
lesson = booking.lesson_sessions[0]
if user.most_recent_test_drive_purchase.nil?
LessonPackagePurchase.create(user, lesson.booking, LessonPackageType.test_drive_4)
LessonPackagePurchase.create(user, lesson.lesson_booking, LessonPackageType.test_drive_4)
end
#lesson.accept({message: 'Yeah I got this', slot: slots[0]})
@ -109,6 +112,6 @@ namespace :lessons do
#lesson.slot.should eql slots[0]
#lesson.status.should eql LessonSession::STATUS_APPROVED
puts "http://localhost:3000/client#/jamclass/lesson-booking/#{lesson.booking.id}"
puts "http://localhost:3000/client#/jamclass/lesson-booking/#{lesson.lesson_booking.id}"
end
end

View File

@ -23,6 +23,21 @@ describe ApiTeachersController do
Teacher.destroy_all
end
describe "index" do
it "simple" do
@teacher = Teacher.save_teacher(
user,
years_teaching: 21,
biography: BIO,
genres: [genre1, genre2],
instruments: [instrument1, instrument2],
languages: [language1, language2],
subjects: [subject1, subject2]
)
get :index
end
end
describe "creates" do
it "simple" do
post :create, biography: BIO, format: 'json'

View File

@ -49,7 +49,7 @@ describe "Test Drive", :js => true, :type => :feature, :capybara_feature => true
# we tell user they have test drive purchased, and take them to the teacher screen
find('#banner h1', text: 'Test Drive Purchased')
find('#banner .dialog-inner', text: "You have purchased #{4} TestDrive credits and have used 1 credit it to request a JamClass with #{teacher_user.name}")
find('#banner .dialog-inner', text: "You have purchased #{4} TestDrive credits and have used 1 credit to request a JamClass with #{teacher_user.name}")
# dismiss banner
find('a.button-orange', text:'CLOSE').trigger(:click)

View File

@ -118,4 +118,95 @@ describe "JamClassScreen", :js => true, :type => :feature, :capybara_feature =>
find('#jam-class-student-screen td.displayStatusColumn', text: 'Canceled (Teacher)')
end
it "student cancels one of recurring" do
lesson = monthly_lesson(user, teacher_user, {accept: true})
lesson1 = lesson.lesson_booking.lesson_sessions[0]
lesson2 = lesson.lesson_booking.lesson_sessions[1]
fast_signin(user, "/client#/jamclass")
find('tr[data-lesson-session-id="' + lesson1.id + '"] td.displayStatusColumn', text: 'Scheduled')
find('tr[data-lesson-session-id="' + lesson2.id + '"] td.displayStatusColumn', text: 'Scheduled')
# open up hover
find('tr[data-lesson-session-id="' + lesson1.id + '"] .lesson-session-actions-btn').trigger(:click)
find('li[data-lesson-option="cancel"] a', visible: false, text: 'Cancel Lesson').trigger(:click)
# confirm cancelation -
find('#banner a', text: 'CANCEL THIS LESSON').trigger(:click)
find('tr[data-lesson-session-id="' + lesson1.id + '"] td.displayStatusColumn', text: 'Canceled (Student)')
find('tr[data-lesson-session-id="' + lesson2.id + '"] td.displayStatusColumn', text: 'Scheduled')
switch_user(teacher_user, "/client#/jamclass")
find('tr[data-lesson-session-id="' + lesson1.id + '"] td.displayStatusColumn', text: 'Canceled (Student)')
find('tr[data-lesson-session-id="' + lesson2.id + '"] td.displayStatusColumn', text: 'Scheduled')
end
it "student cancels all recurring" do
lesson = monthly_lesson(user, teacher_user, {accept: true})
lesson1 = lesson.lesson_booking.lesson_sessions[0]
lesson2 = lesson.lesson_booking.lesson_sessions[1]
lesson1.recurring.should be_true
lesson.lesson_booking.recurring.should be_true
fast_signin(user, "/client#/jamclass")
find('tr[data-lesson-session-id="' + lesson1.id + '"] td.displayStatusColumn', text: 'Scheduled')
find('tr[data-lesson-session-id="' + lesson2.id + '"] td.displayStatusColumn', text: 'Scheduled')
# open up hover
find('tr[data-lesson-session-id="' + lesson1.id + '"] .lesson-session-actions-btn').trigger(:click)
find('li[data-lesson-option="cancel"] a', visible: false, text: 'Cancel Lesson').trigger(:click)
# confirm cancelation -
find('#banner a', text: 'CANCEL ALL LESSONS').trigger(:click)
find('tr[data-lesson-session-id="' + lesson1.id + '"] td.displayStatusColumn', text: 'Canceled (Student)')
lesson1.reload
lesson2.reload
lesson1.status.should eql LessonSession::STATUS_CANCELED
lesson2.status.should eql LessonSession::STATUS_CANCELED
find('tr[data-lesson-session-id="' + lesson2.id + '"] td.displayStatusColumn', text: 'Canceled (Student)')
switch_user(teacher_user, "/client#/jamclass")
find('tr[data-lesson-session-id="' + lesson1.id + '"] td.displayStatusColumn', text: 'Canceled (Student)')
find('tr[data-lesson-session-id="' + lesson2.id + '"] td.displayStatusColumn', text: 'Canceled (Student)')
end
describe "counter" do
it "test drive" do
lesson = testdrive_lesson(user, teacher_user, {accept: false})
fast_signin(teacher_user, "/client#/jamclass")
validate_status(lesson, 'Requested')
jamclass_hover_option('reschedule', 'Reschedule Lesson')
# no popup should show in this case, because it's not yet scheduled
# we should be at lesson status page
find('h2', text: 'respond to lesson request')
screenshot
counter_day
# switch to student
switch_user(user, "/client#/jamclass")
validate_status(lesson, 'Requested')
jamclass_hover_option('status', 'View Status')
find('h2', text: 'this lesson is coming up soon')
screenshot
approve_lesson(lesson)
jamclass_hover_option('reschedule', 'Reschedule Lesson')
find('#banner h1', text: 'Lesson Change Requested')
find('#banner .close-btn').trigger(:click)
counter_day
end
end
end

View File

@ -25,6 +25,7 @@ describe "Lesson Booking Status page", :js => true, :type => :feature, :capybara
find('h2', text: 'your lesson has been requested')
find('p.proposing-new-time')
screenshot
end
@ -32,11 +33,14 @@ describe "Lesson Booking Status page", :js => true, :type => :feature, :capybara
it "approved" do
lesson = testdrive_lesson(user, teacher, {accept:true, finish:false})
fast_signin(user, "/client#/jamclass/lesson-booking/" + lesson.id)
lesson.reload
lesson.status.should
lesson.lesson_booking.accepter_id.should_not be_nil
fast_signin(user, "/client#/jamclass/lesson-booking/" + lesson.id )
find('h2', text: 'this lesson is coming up soon')
find('p.lesson-time', "will take place each")
find('p.generic-time-stmt')
screenshot
end
@ -185,4 +189,90 @@ describe "Lesson Booking Status page", :js => true, :type => :feature, :capybara
find('#lesson-booking', text: 'US Central Time')
end
it "requested recurring with focused lesson" do
lesson = monthly_lesson(user, teacher, {accept: false})
fast_signin(user, "/client#/jamclass/lesson-booking/" + lesson.id)
find('.request-sent', text: 'Your request has been sent.')
page.should_not have_selector('p.proposing-new-time')
# the teacher can either accept or propose a new time, so they see both
switch_user(teacher, "/client#/jamclass/lesson-booking/" + lesson.id)
find('p.action', text: 'Has requested')
page.should_not have_selector('p.generic-time-stmt')
find(".slot-decision-field[data-slot-id=\"#{lesson.lesson_booking.default_slot.id}\"] ins", visible: false).trigger(:click)
find('.schedule.button-orange').trigger(:click)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: 'Scheduled')
end
it "requested recurring with unfocused lesson" do
lesson = monthly_lesson(user, teacher, {accept: false})
fast_signin(user, "/client#/jamclass/lesson-booking/" + lesson.lesson_booking.id)
find('.request-sent', text: 'Your request has been sent.')
page.should_not have_selector('p.proposing-new-time')
switch_user(teacher, "/client#/jamclass/lesson-booking/" + lesson.id)
find('p.action', text: 'Has requested')
page.should_not have_selector('p.generic-time-stmt')
find(".slot-decision-field[data-slot-id=\"#{lesson.lesson_booking.default_slot.id}\"] ins", visible: false).trigger(:click)
find('.schedule.button-orange').trigger(:click)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: 'Scheduled')
end
it "accepted recurring with focused lesson" do
lesson = monthly_lesson(user, teacher, {accept: true})
fast_signin(user, "/client#/jamclass/lesson-booking/" + lesson.id + "_rescheduling")
find('p.proposing-new-time')
# the teacher can either accept or propose a new time, so they see both
switch_user(teacher, "/client#/jamclass/lesson-booking/" + lesson.id + "_rescheduling")
find('p.proposing-new-time')
# change the lesson time
fill_in "alt-date-input", with: date_picker_format(Date.new(Date.today.year, Date.today.month + 1, 17))
find('td a', text: '17').trigger(:click)
sleep 3
find('.schedule.button-orange').trigger(:click)
find('#banner h1', text: 'Lesson Change Requested')
find('#banner .close-btn').trigger(:click)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: 'Requested')
switch_user(user, "/client#/jamclass")
end
it "accepted recurring with unfocused lesson" do
lesson = monthly_lesson(user, teacher, {accept: true})
fast_signin(user, "/client#/jamclass/lesson-booking/" + lesson.lesson_booking.id + "_rescheduling")
find('.request-sent', text: 'Your request has been sent.')
page.should_not have_selector('p.proposing-new-time')
switch_user(teacher, "/client#/jamclass/lesson-booking/" + lesson.lesson_booking.id + "_rescheduling")
find('p.proposing-new-time')
fill_in "alt-date-input", with: date_picker_format(Date.new(Date.today.year, Date.today.month + 1, 17))
find('td a', text: '17').trigger(:click)
sleep 3
find(".slot-decision-field[data-slot-id=\"#{lesson.lesson_booking.default_slot.id}\"] ins", visible: false).trigger(:click)
find('.schedule.button-orange').trigger(:click)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: 'Scheduled')
end
end

View File

@ -29,7 +29,7 @@ describe "Student Landing", :js => true, :type => :feature, :capybara_feature =>
fill_in "email", with: 'student_123@jamkazam.com'
fill_in "password", with: 'jam123'
find('.register-area ins', visible: false) .trigger(:click)
find('.register-area ins', visible: false).trigger(:click)
find('button.cta-button', text: 'SIGN UP').trigger(:click)
# this should show on the /client#/home page (WILL CHANGE)

View File

@ -25,12 +25,16 @@ def teacher_approve(lesson_session)
sign_out_poltergeist(validate: true)
sign_in_poltergeist(lesson_session.teacher, password: 'foobar')
visit "/client#/jamclass/lesson-booking/" + lesson_session.id
find(".slot-decision-field[data-slot-id=\"#{lesson_session.lesson_booking.default_slot.id}\"] ins", visible: false).trigger(:click)
find('.schedule.button-orange').trigger(:click)
# dismiss banner
#find('a.button-orange', text:'CLOSE').trigger(:click)
find('tr[data-lesson-session-id="' + lesson_session.id + '"] .displayStatusColumn', text: 'Scheduled')
end
def date_picker_format(date)
date.strftime('%a %b %d %Y')
end
def fill_out_single_lesson
find('h2', text: 'book testdrive lesson')
@ -56,6 +60,33 @@ def fill_out_single_lesson
end
def validate_status(lesson, expectedStatus)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: expectedStatus)
end
def jamclass_hover_option(lesson, option, text)
# open up hover
find('tr[data-lesson-session-id="' + lesson.id + '"] .lesson-session-actions-btn').trigger(:click)
find('li[data-lesson-option="' + option + '"] a', visible: false, text: text).trigger(:click)
end
def counter_day
fill_in "alt-date-input", with: date_picker_format(Date.new(Date.today.year, Date.today.month + 1, 17))
find('td a', text: '17').trigger(:click)
sleep 3
find('.schedule.button-orange').trigger(:click)
find('#banner h1', text: 'Lesson Change Requested')
find('#banner .close-btn').trigger(:click)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: 'Requested')
end
def approve_lesson(lesson, slot = lesson.lesson_booking.default_slot)
find(".slot-decision-field[data-slot-id=\"#{slot.id}\"] ins", visible: false).trigger(:click)
find('.schedule.button-orange').trigger(:click)
find('tr[data-lesson-session-id="' + lesson.id + '"] td.displayStatusColumn', text: 'Scheduled')
end
def fill_out_payment(expected = nil)
fill_in 'card-number', with: '4111111111111111'
@ -159,7 +190,7 @@ def book_lesson(user, teacher, options)
end
if options[:accept]
lesson.accept({message: 'Yeah I got this', slot: slots[0]})
lesson.accept({message: 'Yeah I got this', slot: slots[0], accepter: teacher})
lesson.errors.any?.should be_false unless options[:no_validate]
lesson.reload
lesson.slot.should eql slots[0] unless options[:no_validate]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB