This commit is contained in:
Seth Call 2016-03-03 13:01:47 -06:00
parent 298c6e5bc5
commit 0e1c070c89
23 changed files with 827 additions and 69 deletions

View File

@ -60,6 +60,8 @@ CREATE TABLE lesson_sessions (
ALTER TABLE music_sessions ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id);
ALTER TABLE notifications ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id);
ALTER TABLE notifications ADD COLUMN purpose VARCHAR(200);
ALTER TABLE notifications ADD COLUMN student_directed BOOLEAN;
INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single', 'Single Lesson', 'A single lesson purchased at the teacher''s price.', 'single', 0.00);
INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single-free', 'Free Lesson', 'A free, single lesson.', 'single-free', 0.00);
@ -68,19 +70,22 @@ INSERT INTO lesson_package_types (id, name, description, package_type, price) VA
CREATE TABLE lesson_booking_slots (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id) NOT NULL,
lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id),
lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id),
slot_type VARCHAR(64) NOT NULL,
preferred_day DATE,
day_of_week INTEGER,
hour INTEGER,
minute INTEGER,
timezone VARCHAR NOT NULL,
update_all BOOLEAN NOT NULL DEFAULT FALSE,
proposer_id VARCHAR(64) REFERENCES users(id) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id);
ALTER TABLE lesson_sessions ADD COLUMN slot VARCHAR(64) REFERENCES lesson_booking_slots(id);
ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id);
ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id);
ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id);

View File

@ -652,13 +652,13 @@ message MixdownSignFailed {
}
message LessonMessage {
optional string lesson_session_id = 1;
optional string music_session_id = 1;
optional string photo_url = 2;
optional string msg = 3;
optional string notification_id = 4;
optional string created_at = 5;
optional string sender_id = 6;
optional string received_id = 7;
optional string receiver_id = 7;
optional bool student_directed = 8;
optional string purpose = 9;
optional string sender_name = 10;

View File

@ -55,6 +55,7 @@ gem 'sendgrid_toolkit', '>= 1.1.1'
gem 'stripe'
gem 'zip-codes'
gem 'icalendar'
gem 'timespan'
group :test do
gem 'simplecov', '~> 0.7.1'

View File

@ -278,6 +278,7 @@ require "jam_ruby/models/jamblaster"
require "jam_ruby/models/jamblaster_user"
require "jam_ruby/models/jamblaster_pairing_request"
require "jam_ruby/models/sale_receipt_ios"
require "jam_ruby/models/lesson_session_analyser"
include Jampb

View File

@ -751,5 +751,54 @@
format.html
end
end
# teacher proposed counter time; so send msg to the student
def student_lesson_counter(lesson_session, slot)
email = lesson_session.student.email
subject = "Instructor has proposed a different time for your lesson"
unique_args = {:type => "student_lesson_counter"}
@student = lesson_session.student
@teacher = lesson_session.teacher
@session_name = lesson_session.music_session.name
@session_description = lesson_session.music_session.description
@session_date = slot.pretty_scheduled_start(true)
@session_url = lesson_session.web_url
@lesson_session = lesson_session
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@student.id])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
end
end
# student proposed counter time; so send msg to the teacher
def teacher_lesson_counter(lesson_session, slot)
email = lesson_session.teacher.email
subject = "Student has proposed a different time for their lesson"
unique_args = {:type => "teacher_lesson_counter"}
@student = lesson_session.student
@teacher = lesson_session.teacher
@session_name = lesson_session.music_session.name
@session_description = lesson_session.music_session.description
@session_date = slot.pretty_scheduled_start(true)
@session_url = lesson_session.web_url
@lesson_session = lesson_session
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@teacher.id])
mail(:to => email, :subject => subject) do |format|
format.text
format.html
end
end
end
end

View File

@ -0,0 +1,17 @@
<% provide(:title, "#{@teacher.name} has proposed a different time for your lesson") %>
<% provide(:photo_url, @teacher.resolved_photo_url) %>
<% content_for :note do %>
<p>
<%= @teacher.name %> has proposed a different time for your lesson request.
<br/>
<br/>
Click the button below to get more information and respond.
</p>
<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,3 @@
<%= @teacher.name %> has proposed a different time for your lesson request.
To see this lesson, click here: <%= @lesson_session.web_url %>

View File

@ -0,0 +1,17 @@
<% provide(:title, "#{@student.name} has proposed a different time for their lesson") %>
<% provide(:photo_url, @student.resolved_photo_url) %>
<% content_for :note do %>
<p>
<%= @student.name %> has proposed a different time for their lesson request.
<br/>
<br/>
Click the button below to get more information and respond.
</p>
<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,3 @@
<%= @student.name %> has proposed a different time for their lesson request.
To see this lesson, click here: <%= @lesson_session.web_url %>

View File

@ -917,7 +917,7 @@ module JamRuby
end
# creates the general purpose text message
def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, lesson_session_id, created_at)
def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose)
lesson_message = Jampb::LessonMessage.new(
:photo_url => sender_photo_url,
:sender_name => sender_name,
@ -925,8 +925,10 @@ module JamRuby
:receiver_id => receiver_id,
:msg => msg,
:notification_id => notification_id,
:lesson_session_id => lesson_session_id,
:created_at => created_at
:music_session_id => music_session_id,
:created_at => created_at,
:student_directed => student_directed,
:purpose => purpose
)
Jampb::ClientMessage.new(

View File

@ -4,7 +4,7 @@ module JamRuby
@@log = Logging.logger[LessonBooking]
attr_accessor :accepting
attr_accessor :accepting, :countering, :countered_slot, :countered_lesson
STATUS_REQUESTED = 'requested'
STATUS_CANCELED = 'canceled'
@ -51,6 +51,7 @@ module JamRuby
validate :validate_payment_style
validate :validate_accepted, :if => :accepting
before_save :before_save
before_validation :before_validation
after_create :after_create
@ -70,23 +71,31 @@ module JamRuby
def before_save
automatically_default_slot
sync_remaining_test_drives
end
def after_save
sync_lessons
sync_remaining_test_drives
end
def student
user
end
def accept(lesson_session, slot, update_all)
def accept(lesson_session, slot)
self.accepting = true
self.default_slot = slot
if self.default_slot.nil? || update_all
self.accepting = true
self.default_slot = slot
self.save!
if !self.save
raise ActiveRecord::Rollback
end
end
def counter(lesson_session, proposer, slot)
self.countering = true
self.lesson_booking_slots << slot
if !self.save
raise ActiveRecord::Rollback
end
end
@ -100,7 +109,6 @@ module JamRuby
def sync_remaining_test_drives
if is_test_drive? || is_single_free?
puts "CARD PRESUMED OK card_presumed_ok: #{card_presumed_ok}, user_decremented: #{user_decremented}"
if card_presumed_ok && !user_decremented
self.user_decremented = true
@ -110,7 +118,6 @@ module JamRuby
user.remaining_free_lessons = remaining_free_lessons
elsif is_test_drive?
puts "TEST DRIVE DECREMENT"
remaining_test_drives = user.remaining_test_drives - 1
User.where(id: user.id).update_all(remaining_test_drives: remaining_test_drives)
user.remaining_test_drives = remaining_test_drives
@ -120,6 +127,10 @@ module JamRuby
end
end
def create_minimum_booking_time
(Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60)
end
def sync_lessons
if is_canceled?
@ -129,14 +140,20 @@ module JamRuby
if !recurring && lesson_sessions.count == 1
# if not recurring, and there is already one lession created, we are good
# if not recurring, and there is already one lesson created, then at most we may have to deal with a changed slot
if default_slot_changed?
if !lesson_sessions[0].update_scheduled_start(0)
puts "unable to update scheduled lesson"
raise ActiveRecord::Rollback
end
end
return
end
# Here we go; let's create a lesson(s) as needed
# we need to make lessons into the future a bit, to give time for everyone involved
minimum_start_time = (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60)
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)
@ -225,11 +242,11 @@ module JamRuby
def send_notices
UserMailer.student_lesson_request(self).deliver
UserMailer.teacher_lesson_request(self).deliver
LessonBooking.where(id: id).update_all(sent_notices: true)
self.sent_notices = true
self.save
end
def lesson_package_type
if is_single_free?
LessonPackageType.single_free
elsif is_test_drive?
@ -371,15 +388,20 @@ module JamRuby
lesson_booking.sent_notices = false
lesson_booking.teacher = teacher
lesson_booking.lesson_type = lesson_type
lesson_booking.lesson_booking_slots = lesson_booking_slots
lesson_booking.recurring = recurring
lesson_booking.lesson_length = lesson_length
lesson_booking.payment_style = payment_style
lesson_booking.description = description
lesson_booking.status = STATUS_REQUESTED
# two-way association slots, for before_validation loic in slot to work
lesson_booking.lesson_booking_slots = lesson_booking_slots
lesson_booking_slots.each do |slot|
slot.lesson_booking = lesson_booking
end if lesson_booking_slots
if lesson_booking.save
msg = ChatMessage.create(user, nil, description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking)
msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking)
end
end
lesson_booking

View File

@ -5,7 +5,8 @@ module JamRuby
@@log = Logging.logger[LessonBookingSlot]
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
belongs_to :lesson_session, class_name: "JamRuby::LessonSession"
belongs_to :proposer, class_name: "JamRuby::User"
has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot
SLOT_TYPE_SINGLE = 'single'
@ -13,14 +14,49 @@ module JamRuby
SLOT_TYPES = [SLOT_TYPE_SINGLE, SLOT_TYPE_RECURRING]
validates :proposer, presence: true
validates :slot_type, inclusion: {in: SLOT_TYPES}
#validates :preferred_day
validates :day_of_week, numericality: {only_integer: true}, allow_blank: true # 0 = sunday - 6 = saturday
validates :hour, numericality: {only_integer: true}
validates :minute, numericality: {only_integer: true}
validates :timezone, presence: true # example: 'America/New_York'
validates :update_all, inclusion: {in: [true, false]}
validate :validate_slot_type
validate :validate_slot_minimum_time, on: :create
validate :validate_proposer
before_validation :before_validation
def before_validation
if proposer.nil?
self.proposer = container.student
end
end
def container
if lesson_booking
lesson_booking
else
lesson_session
end
end
def is_teacher_created?
self.proposer == container.teacher
end
def recipient
if is_teacher_created?
container.student
else
container.teacher
end
end
def create_minimum_booking_time
(Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60 * 60)
end
def scheduled_times(needed_sessions, minimum_start_time)
@ -73,6 +109,52 @@ module JamRuby
time
end
def lesson_length
safe_lesson_booking.lesson_length
end
def safe_lesson_booking
found = lesson_booking
found ||= lesson_session.lesson_booking
end
def pretty_scheduled_start(with_timezone)
start_time = scheduled_time(0)
begin
tz = TZInfo::Timezone.get(timezone)
rescue Exception => e
@@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
end
if tz
begin
start_time = tz.utc_to_local(start_time)
rescue Exception => e
@@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}")
puts "unable to convert #{e}"
end
end
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}"
else
"#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}"
end
end
def validate_proposer
if proposer && (proposer != container.student && proposer != container.teacher)
errors.add(:proposer, "must be either the student or teacher")
end
end
def validate_slot_type
if slot_type == SLOT_TYPE_SINGLE
if preferred_day.nil?
@ -86,5 +168,23 @@ module JamRuby
end
end
end
def validate_slot_minimum_time
if is_teacher_created?
return # the thinking is that a teacher can propose much tighter to the time; since they only counter; maybe they talked to the student
end
minimum_start_time = create_minimum_booking_time
if day_of_week
# this is recurring; it will sort itself out
else
time = scheduled_time(0)
if time <= minimum_start_time
errors.add("must be at least #{APP_CONFIG.minimum_lesson_booking_hrs} hours out from now")
end
end
end
end
end

View File

@ -24,7 +24,7 @@ module JamRuby
if self.lesson_package_type.is_test_drive?
new_test_drives = user.remaining_test_drives + 4
User.where(id:user.id).update_all(remaining_test_drives: new_test_drives)
user.remaining_test_drives = user.remaining_test_drives + 4
user.remaining_test_drives = new_test_drives
end
end

View File

@ -3,7 +3,7 @@ module JamRuby
class LessonSession < ActiveRecord::Base
attr_accessor :accepting, :creating
attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson
@@log = Logging.logger[LessonSession]
@ -24,6 +24,8 @@ module JamRuby
belongs_to :teacher, class_name: "JamRuby::User"
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot"
validates :duration, presence: true, numericality: {only_integer: true}
validates :lesson_booking, presence: true
@ -37,6 +39,26 @@ module JamRuby
validate :validate_creating, :if => :creating
validate :validate_accepted, :if => :accepting
after_save :after_counter, :if => :countering
after_save :manage_slot_changes, :if => :slot_changed?
def manage_slot_changes
# if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted .
# TODO: what to do, what to do.
end
def after_counter
send_counter(@countered_lesson, @countered_slot)
end
def send_counter(countered_lesson, countered_slot)
if countered_slot.is_teacher_created?
UserMailer.student_lesson_counter(countered_lesson, countered_slot).deliver
else
UserMailer.teacher_lesson_counter(countered_lesson, countered_slot).deliver
end
self.countering = false
end
default_scope { order(:created_at) }
@ -56,6 +78,7 @@ module JamRuby
status == STATUS_APPROVED
end
def validate_creating
if !is_requested? && !is_approved?
self.errors.add(:status, "is not valid for a new lesson session.")
@ -67,6 +90,7 @@ module JamRuby
self.errors.add(:status, "This session is already #{self.status}.")
end
self.accepting = false
self.status = STATUS_APPROVED
end
@ -124,30 +148,57 @@ module JamRuby
end
end
def update_scheduled_start(week_offset)
music_session.scheduled_start = default_slot.scheduled_time(week_offset)
music_session.save
end
# teacher accepts the lesson
def accept(params)
LessonSession.transaction do
message = params[:message]
slot = params[:slot]
update_all = params[:update_all]
self.accepting = true
slot = LessonBookingSlot.find(slot)
lesson_booking.accept(self, slot, update_all)
lesson_booking.accept(self, slot)
self.slot = slot
if self.save
msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, user, self)
msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking)
Notification.send_lesson_message('accept', self, true)
UserMailer.student_lesson_accepted(self, message)
UserMailer.teacher_lesson_accepted(self, message)
UserMailer.student_lesson_accepted(self, message).deliver
UserMailer.teacher_lesson_accepted(self, message).deliver
end
end
end
def counter(params)
proposer = params[:proposer]
slot = params[:slot]
message = params[:message]
self.countering = true
slot.proposer = proposer
slot.lesson_session = self
self.lesson_booking_slots << slot
self.countered_slot = slot
self.countered_lesson = self
if self.save
if slot.update_all || lesson_booking.is_requested?
lesson_booking.counter(self, proposer, slot)
end
else
raise ActiveRecord::Rollback
end
msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking)
Notification.send_lesson_message('counter', self, slot.is_teacher_created?)
end
def home_url
APP_CONFIG.external_root_url + "/client#/jamclass"
end

View File

@ -0,0 +1,248 @@
require 'timespan'
module JamRuby
class LessonSessionAnalyser
SUCCESS = 'success'
SESSION_ONGOING = 'session_ongoing'
THRESHOLD_MET = 'threshold_met'
WAITED_CORRECTLY = 'waited_correctly'
MINIMUM_TIME_MET = 'minimum_time_met' # for a teacher primarily; they waited around for the student sufficiently
LATE_CANCELLATION = 'late_cancellation'
TEACHER_FAULT = 'teacher_fault'
STUDENT_FAULT = 'student_fault'
BOTH_FAULT = 'both_fault'
# what are the potential results?
# bill: true/false
# teacher: 'no_show'
# teacher: 'late'
# teacher: 'early_leave'
# teacher: 'waited_correctly'
# teacher: 'late_cancellation'
# student: 'no_show'
# student: 'late'
# student: 'early_leave'
# student: 'minimum_time_not_met'
# student: 'threshold_met'
# reason: 'session_ongoing'
# reason: 'success'
# reason: 'student_fault'
# reason: 'teacher_fault'
# reason: 'both_fault'
def self.analyse(lesson_session)
reason = nil
teacher = nil
student = nil
bill = false
music_session = lesson_session.music_session
student_histories = MusicSessionUserHistory.where(music_session: music_session, user: lesson_session.student)
teacher_histories = MusicSessionUserHistory.where(music_session: music_session, user: lesson_session.teacher)
# create ranges from music session user history
all_student_ranges = time_ranges(student_histories)
all_teacher_ranges = time_ranges(teacher_histories)
# flatten ranges into non-overlapping ranges to simplifly logic
student_ranges = merge_overlapping_ranges(all_student_ranges)
teacher_ranges = merge_overlapping_ranges(all_teacher_ranges)
intersecting = intersecting_ranges(student_ranges, teacher_ranges)
student_analysis = analyse_intersection(lesson_session, student_ranges)
teacher_analysis = analyse_intersection(lesson_session, teacher_ranges)
together_analysis = analyse_intersection(lesson_session, intersecting)
# spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest
if music_session.session_removed_at.nil?
reason = SESSION_ONGOING
teacher = SESSION_ONGOING
student = SESSION_ONGOING
bill = false
else
if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late?
# If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student.
reason = TEACHER_FAULT
teacher = LATE_CANCELLATION
student = nil
bill = false
elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late?
# If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I cant remember now), then we do bill the student.
reason = STUDENT_FAULT
teacher = nil
student = LATE_CANCELLATION
bill = true
elsif together_analysis[:pct] >= APP_CONFIG.lesson_together_threshold_pct
# If the teacher and the student are together in the lesson session for at least 80% of the scheduled lesson duration, regardless of who joined/left when, then we will bill the student.
bill = true
reason = SUCCESS
teacher = THRESHOLD_MET
student = THRESHOLD_MET
else
if teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly]
# if the teacher was present in the session within the first 5 minutes of the scheduled start time and stayed in the session for 10 minutes;
# and if either:
if student_analysis[:no_show]
# the student no-showed entirely, then we bill the student.
elsif student_analysis[:joined_late]
# the student joined the lesson more than 10 minutes after the teacher did, regardless of whether the teacher was still in the lesson session at that point; then we bill the student
elsif student_analysis[:joined_on_time]
# the student joined the session within 10 minutes after the teacher did, and the teacher was still there, and the teacher stays until the scheduled end time of the lesson; then we bill the student.
if teacher_analysis[:minimum_time_met] && teacher_analysis[:there_when_other_joined]
else
end
else
raise "unknown condition for lesson session: #{lesson_session.id}"
end
elsif student_analysis[:joined_on_time]
if student_analysis[:waited_correctly]
if teacher_analysis[:other_there_when_joined]
# bill the student
else
end
end
end
# if the teacher meets the time threshold, then the user is getting billed
teacher = MINIMUM_TIME_MET
bill = true
# What happened with the student?
if student_analysis[:waited_correctly]
student = WAITED_CORRECTLY
reason = STUDENT_FAULT
elsif student_analysis[:time] == 0
student = NO_SHOW
reason = STUDENT_FAULT
elsif student_analysis
end
end
end
{
reason: reason,
teacher: teacher,
student: student,
bill: bill,
student_ranges: student_ranges,
teacher_ranges: teacher_ranges,
intersecting: intersecting,
student_analysis: student_analysis,
teacher_analysis: teacher_analysis,
together_analysis: together_analysis,
}
end
def self.intersecting_ranges(ranges_a, ranges_b)
intersections = []
ranges_a.each do |range_a|
ranges_b.each do |range_b|
intersection = intersect(range_a, range_b)
intersections << intersection if intersection
end
end
merge_overlapping_ranges(intersections)
end
def self.analyse_intersection(lesson_session, ranges)
start = lesson_session.music_session.scheduled_start
planned_duration_seconds = lesson_session.duration * 60
measurable_start = start - (APP_CONFIG.lesson_analysis_slush_time_minutes * 60)
measurable_end = (start + planned_duration_seconds) + (APP_CONFIG.lesson_analysis_slush_time_minutes * 60)
session_time = Range.new(measurable_start, measurable_end)
# let's see how much time they spent together, irrespective of scheduled time
# and also, based on scheduled time
total = 0
in_scheduled_time = 0
ranges.each do |range|
time = range.end - range.begin
total += time
in_session_time = intersect(range, session_time)
if in_session_time
in_scheduled_time += in_scheduled_time.end - in_scheduled_time.begin
end
end
# percentage computation of time spent during the session time
in_scheduled_percentage = in_scheduled_time.to_f / planned_duration_seconds.to_f
{
total: total,
time: in_scheduled_time,
pct: in_scheduled_percentage
}
end
def self.intersect(a, b)
min, max = a.first, a.exclude_end? ? a.max : a.last
other_min, other_max = b.first, b.exclude_end? ? b.max : b.last
new_min = a === other_min ? other_min : b === min ? min : nil
new_max = a === other_max ? other_max : b === max ? max : nil
new_min && new_max ? Range.new(new_min, new_max) : nil
end
def self.time_ranges(histories)
ranges = []
histories.each do |history|
ranges << history.range
end
ranges
end
def self.ranges_overlap?(a, b)
a.include?(b.begin) || b.include?(a.begin)
end
def self.merge_ranges(a, b)
[a.begin, b.begin].min..[a.end, b.end].max
end
def self.merge_overlapping_ranges(ranges)
ranges.sort_by(&:begin).inject([]) do |ranges, range|
if !ranges.empty? && ranges_overlap?(ranges.last, range)
ranges[0...-1] + [merge_ranges(ranges.last, range)]
else
ranges + [range]
end
end
end
end
end

View File

@ -25,6 +25,10 @@ module JamRuby
user.name
end
def range
Range.new(created_at, session_removed_at || Time.now)
end
def music_session
@msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id)
end

View File

@ -377,17 +377,17 @@ module JamRuby
notification = Notification.new
notification.description = NotificationTypes::LESSON_MESSAGE
notification.student_directed = student_directed
if !student_directed
notification.source_user_id = lesson_session.user.id
notification.source_user_id = lesson_session.student.id
notification.target_user_id = lesson_session.teacher.id
else
notification.source_user_id = lesson_session.teacher.id
notification.target_user_id = lesson_session.user.id
notification.target_user_id = lesson_session.student.id
end
notification.purpose = purpose
notification.lesson_session = lesson_session
notification.session_id = lesson_session.music_session_id
notification.session_id = lesson_session.music_session.id
notification_msg = 'Lesson Changed'
@ -412,37 +412,21 @@ module JamRuby
notification.save
# receiver_id, sender_photo_url, sender_name, sender_id, msg, clipped_msg, notification_id, created_at
message = @message_factory.lesson_message(
notification.target.id,
notification.source.resolved_photo_url,
notification.source.name,
notification.source.id,
message = @@message_factory.lesson_message(
notification.target_user.id,
notification.source_user.resolved_photo_url,
notification.source_user.name,
notification.source_user.id,
notification_msg,
notification.id,
notification.lesson_session.id,
notification.created_date
notification.session_id,
notification.created_date,
notification.student_directed,
notification.purpose
)
if follower.id != user.id
if user.online
msg = @@message_factory.new_user_follower(
user.id,
follower.photo_url,
notification_msg,
notification.id,
notification.created_date
)
@@mq_router.publish_to_user(notification.target_user.id, message)
@@mq_router.publish_to_user(user.id, msg)
else
begin
UserMailer.new_user_follower(user, notification_msg).deliver
rescue => e
@@log.error("Unable to send NEW_USER_FOLLOWER email to offline user #{user.email} #{e}")
end
end
end
end
def send_new_band_follower(follower, band)

View File

@ -1898,8 +1898,10 @@ module JamRuby
lesson = nil
test_drive = nil
User.transaction do
lesson = card_approved(params[:token], params[:zip])
if params[:test_drive]
self.reload
test_drive = Sale.purchase_test_drive(self)
end
end

View File

@ -921,7 +921,7 @@ FactoryGirl.define do
factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do
factory :lesson_booking_slot_single do
slot_type 'single'
preferred_day Date.today
preferred_day Date.today + 3
day_of_week nil
hour 12
minute 30

View File

@ -15,23 +15,26 @@ describe "TestDrive Lesson Flow" do
it "works" do
# user has no test drives, but attempts to book a lesson
# user has no test drives, no credit card on file, but attempts to book a lesson
booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.")
puts booking.errors.inspect
booking.errors.any?.should be_false
booking.card_presumed_ok.should be_false
booking.user.should eql user
booking.card_presumed_ok.should be_false
booking.should eql user.unprocessed_test_drive
booking.sent_notices.should be_false
# so, they still need validate their take
########## Need validate their credit card
token = create_stripe_token
puts "UPDATING PAYMENT"
result = user.payment_update({token: token, zip: '78759', test_drive: true})
lesson = result[:lesson]
test_drive = result[:test_drive]
lesson.errors.any?.should be_false
test_drive.errors.any?.should be_false
lesson.card_presumed_ok.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
booking.reload
booking.sent_notices.should be_true
test_drive.stripe_charge_id.should_not be_nil
test_drive.recurly_tax_in_cents.should be 412
@ -42,7 +45,6 @@ describe "TestDrive Lesson Flow" do
line_item.quantity.should eql 1
line_item.product_type.should eql SaleLineItem::LESSON
line_item.product_id.should eq LessonPackageType.test_drive.id
user.reload
user.stripe_customer_id.should_not be nil
user.lesson_purchases.length.should eql 1
@ -53,5 +55,90 @@ describe "TestDrive Lesson Flow" do
customer = Stripe::Customer.retrieve(user.stripe_customer_id)
customer.email.should eql user.email
booking.lesson_sessions.length.should eql 1
lesson_session = booking.lesson_sessions[0]
lesson_session.status.should eql LessonBooking::STATUS_REQUESTED
booking.status.should eql LessonBooking::STATUS_REQUESTED
######### Teacher counters with new slot
teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14)
UserMailer.deliveries.clear
lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'})
booking.errors.any?.should be false
lesson_session.lesson_booking.errors.any?.should be false
lesson_session.lesson_booking_slots.length.should eql 1
lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user
teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last
teacher_counter.should eql teacher_countered_slot
teacher_counter.proposer.should eql teacher_user
booking.lesson_booking_slots.length.should eql 3
UserMailer.deliveries.length.should eql 1
chat = ChatMessage.unscoped.order(:created_at).last
chat.channel.should eql ChatMessage::CHANNEL_LESSON
chat.message.should eql 'Does this work?'
chat.user.should eql teacher_user
chat.target_user.should eql user
notification = Notification.unscoped.order(:created_at).last
notification.session_id.should eql lesson_session.music_session.id
notification.student_directed.should eql true
notification.purpose.should eql 'counter'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
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)
UserMailer.deliveries.clear
lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'})
lesson_session.errors.any?.should be false
lesson_session.lesson_booking.errors.any?.should be false
lesson_session.lesson_booking_slots.length.should eql 2
student_counter = booking.lesson_booking_slots.order(:created_at).last
student_counter.proposer.should eql user
booking.reload
booking.lesson_booking_slots.length.should eql 4
UserMailer.deliveries.length.should eql 1
chat = ChatMessage.unscoped.order(:created_at).last
chat.message.should eql 'Does this work better?'
chat.channel.should eql ChatMessage::CHANNEL_LESSON
chat.user.should eql user
chat.target_user.should eql teacher_user
notification = Notification.unscoped.order(:created_at).last
notification.session_id.should eql lesson_session.music_session.id
notification.student_directed.should eql false
notification.purpose.should eql 'counter'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
notification.message.should eql "Student has proposed a different time for your lesson."
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.slot.should eql student_counter
lesson_session.status.should eql LessonSession::STATUS_APPROVED
booking.reload
booking.default_slot.should eql student_counter
lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
booking.status.should eql LessonBooking::STATUS_APPROVED
UserMailer.deliveries.length.should eql 2
chat = ChatMessage.unscoped.order(:created_at).last
chat.message.should eql 'Yeah I got this'
chat.channel.should eql ChatMessage::CHANNEL_LESSON
chat.user.should eql teacher_user
chat.target_user.should eql user
notification = Notification.unscoped.order(:created_at).last
notification.session_id.should eql lesson_session.music_session.id
notification.student_directed.should eql true
notification.purpose.should eql 'accept'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
notification.message.should eql "Your lesson request is confirmed!"
# teacher & student get into session
LessonSession
end
end

View File

@ -0,0 +1,146 @@
require 'spec_helper'
describe LessonSessionAnalyser do
let(:user) {FactoryGirl.create(:user)}
let(:music_session) { FactoryGirl.create(:music_session, creator: user) }
let(:start) {Time.now}
describe "intersecting_ranges" do
it "empty" do
LessonSessionAnalyser.intersecting_ranges([], []).should eql []
end
it "one specified, other empty" do
uh1Begin = start
uh1End = start + 1
LessonSessionAnalyser.intersecting_ranges([], [Range.new(uh1Begin, uh1End)]).should eql []
LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], []).should eql []
end
it "both identical" do
uh1Begin = start
uh1End = start + 1
LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh1End)]
end
it "one intersect" do
uh1Begin = start
uh1End = start + 4
uh2Begin = start + 1
uh2End = start + 3
LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [Range.new(uh2Begin, uh2End)]
end
it "overlapping intersect" do
uh1Begin = start
uh1End = start + 4
uh2Begin = start + -1
uh2End = start + 2
uh3Begin = start + 3
uh3End = start + 5
LessonSessionAnalyser.intersecting_ranges([Range.new(uh2Begin, uh2End), Range.new(uh3Begin, uh3End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh2End), Range.new(uh3Begin, uh1End)]
end
it "no overlap" do
uh1Begin = start
uh1End = start + 4
uh2Begin = start + 5
uh2End = start + 6
LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql []
end
end
describe "merge_overlapping_ranges" do
it "empty" do
LessonSessionAnalyser.merge_overlapping_ranges([]).should eql []
end
it "one item" do
uh1Begin = start
uh1End = start + 1
uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End)
ranges = LessonSessionAnalyser.time_ranges [uh1]
LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)]
end
it "two identical items" do
uh1Begin = start
uh1End = start + 1
uh2Begin = uh1Begin
uh2End = uh1End
uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End)
uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End)
ranges = LessonSessionAnalyser.time_ranges [uh1, uh2]
LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)]
end
it "two separate times" do
uh1Begin = start
uh1End = start + 1
uh2Begin = start + 3
uh2End = start + 5
uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End)
uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End)
ranges = LessonSessionAnalyser.time_ranges [uh1, uh2]
LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End), Range.new(uh2Begin, uh2End)]
end
it "two overlapping times" do
uh1Begin = start
uh1End = start + 1
uh2Begin = start + 0.5
uh2End = start + 5
uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End)
uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End)
ranges = LessonSessionAnalyser.time_ranges [uh1, uh2]
LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh2End)]
end
it "three overlapping times" do
uh1Begin = start
uh1End = start + 1
uh2Begin = start + 0.5
uh2End = start + 5
uh3Begin = start + 0.1
uh3End = start + 6
uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End)
uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End)
uh3 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh3Begin, session_removed_at: uh3End)
ranges = LessonSessionAnalyser.time_ranges [uh1, uh2, uh3]
LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh3End)]
end
end
end

View File

@ -259,7 +259,19 @@ def app_config
end
def minimum_lesson_booking_hrs
48
24
end
def lesson_analysis_slush_time_minutes
5
end
def lesson_stay_time
10
end
def lesson_together_threshold_pct
0.8
end
private

View File

@ -426,6 +426,10 @@ if defined?(Bundler)
:publishable_key => 'pk_test_9vO8ZnxBpb9Udb0paruV3qLv',
:secret_key => 'sk_test_cPVRbtr9xbMiqffV8jwibwLA'
}
config.minimum_lesson_booking_hrs = 48
end
config.minimum_lesson_booking_hrs = 24
config.lesson_analysis_slush_time_minutes = 5
config.lesson_stay_time = 10
config.lesson_together_threshold_pct = 0.80
end
end