2016-04-06 02:23:15 +00:00
|
|
|
# represenst the type of lesson package
|
|
|
|
|
module JamRuby
|
|
|
|
|
class LessonBookingSlot < ActiveRecord::Base
|
|
|
|
|
|
|
|
|
|
include HtmlSanitize
|
|
|
|
|
html_sanitize strict: [:message, :accept_message, :cancel_message]
|
|
|
|
|
|
|
|
|
|
@@log = Logging.logger[LessonBookingSlot]
|
|
|
|
|
|
|
|
|
|
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
|
|
|
|
|
belongs_to :lesson_session, class_name: "JamRuby::LessonSession"
|
2016-08-03 01:46:15 +00:00
|
|
|
belongs_to :proposer, class_name: "JamRuby::User", inverse_of: :proposed_slots, foreign_key: :proposer_id
|
2016-04-06 02:23:15 +00:00
|
|
|
has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot
|
|
|
|
|
has_one :countered_booking, class_name: "JamRuby::LessonBooking", foreign_key: :counter_slot_id, inverse_of: :counter_slot
|
|
|
|
|
has_one :countered_lesson, class_name: "JamRuby::LessonSession", foreign_key: :counter_slot_id, inverse_of: :counter_slot
|
|
|
|
|
|
|
|
|
|
SLOT_TYPE_SINGLE = 'single'
|
|
|
|
|
SLOT_TYPE_RECURRING = 'recurring'
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2016-06-01 00:20:03 +00:00
|
|
|
def is_recurring?
|
|
|
|
|
slot_type == SLOT_TYPE_RECURRING
|
|
|
|
|
end
|
2016-04-06 02:23:15 +00:00
|
|
|
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 is_student_created?
|
|
|
|
|
!is_teacher_created?
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def is_teacher_approved?
|
|
|
|
|
!is_teacher_created?
|
|
|
|
|
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
|
|
|
|
|
|
2016-06-03 04:32:09 +00:00
|
|
|
# create a canned slot for a TestDrivePackage. The most important thing here is that it expires in 30 days
|
|
|
|
|
def self.packaged_slots
|
|
|
|
|
slot = LessonBookingSlot.new
|
|
|
|
|
slot.from_package = true
|
|
|
|
|
slot.preferred_day = Date.today + 30
|
|
|
|
|
slot.slot_type = LessonBookingSlot::SLOT_TYPE_SINGLE
|
|
|
|
|
slot.hour = 1
|
|
|
|
|
slot.minute = 0
|
|
|
|
|
slot.timezone = 'America/Chicago'
|
|
|
|
|
[slot]
|
|
|
|
|
end
|
|
|
|
|
|
2016-04-06 02:23:15 +00:00
|
|
|
def scheduled_times(needed_sessions, minimum_start_time)
|
|
|
|
|
|
2016-04-26 17:36:06 +00:00
|
|
|
#puts "NEEDED SESSIONS #{needed_sessions} #{minimum_start_time}"
|
2016-04-06 02:23:15 +00:00
|
|
|
times = []
|
|
|
|
|
week_offset = 0
|
|
|
|
|
|
|
|
|
|
needed_sessions.times do |i|
|
|
|
|
|
candidate = scheduled_time(i + week_offset)
|
|
|
|
|
|
2016-04-26 17:36:06 +00:00
|
|
|
#puts "#{i}: candidate #{candidate} week_offset:#{week_offset}"
|
2016-05-31 16:13:49 +00:00
|
|
|
#puts "DAY_OF_WEEK #{day_of_week}"
|
2016-04-06 02:23:15 +00:00
|
|
|
if day_of_week && candidate <= minimum_start_time
|
|
|
|
|
# move it up a week
|
|
|
|
|
week_offset += 1
|
|
|
|
|
candidate = scheduled_time(i + week_offset)
|
|
|
|
|
|
2016-04-26 17:36:06 +00:00
|
|
|
#puts "retry #1 #{candidate}"
|
2016-04-06 02:23:15 +00:00
|
|
|
# sanity check
|
|
|
|
|
if candidate <= minimum_start_time
|
|
|
|
|
week_offset += 1
|
|
|
|
|
candidate = scheduled_time(i + week_offset)
|
|
|
|
|
|
2016-04-26 17:36:06 +00:00
|
|
|
#puts "retry #2 #{candidate}"
|
2016-04-06 02:23:15 +00:00
|
|
|
if candidate <= minimum_start_time
|
2016-04-26 17:36:06 +00:00
|
|
|
|
|
|
|
|
week_offset += 1
|
|
|
|
|
candidate = scheduled_time(i + week_offset)
|
|
|
|
|
|
|
|
|
|
#puts "retry #3 #{candidate}"
|
|
|
|
|
if candidate <= minimum_start_time
|
|
|
|
|
|
|
|
|
|
week_offset += 1
|
|
|
|
|
candidate = scheduled_time(i + week_offset)
|
|
|
|
|
|
|
|
|
|
#puts "retry #4 #{candidate}"
|
|
|
|
|
if candidate <= minimum_start_time
|
|
|
|
|
raise "candidate time less than minimum start time even after scoot: #{lesson_booking.id} #{self.id}"
|
|
|
|
|
end
|
|
|
|
|
end
|
2016-04-06 02:23:15 +00:00
|
|
|
end
|
2016-04-26 17:36:06 +00:00
|
|
|
|
2016-04-06 02:23:15 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
times << candidate
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
times
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def next_day
|
|
|
|
|
date = Date.today
|
|
|
|
|
date += ((day_of_week - date.wday) % 7).abs
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# weeks is the number of weeks in the future to compute the time for
|
|
|
|
|
def scheduled_time(weeks)
|
|
|
|
|
|
|
|
|
|
# get the timezone of the slot, so we can compute times
|
|
|
|
|
tz = TZInfo::Timezone.get(timezone)
|
|
|
|
|
|
|
|
|
|
if preferred_day
|
|
|
|
|
time = tz.local_to_utc(Time.new(preferred_day.year, preferred_day.month, preferred_day.day, hour, minute, 0))
|
|
|
|
|
else
|
|
|
|
|
adjusted = next_day + (weeks * 7)
|
|
|
|
|
# day of the week adjustment
|
|
|
|
|
time = tz.local_to_utc(Time.new(adjusted.year, adjusted.month, adjusted.day, hour, minute, 0))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
time
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def lesson_length
|
|
|
|
|
safe_lesson_booking.lesson_length
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def safe_lesson_booking
|
|
|
|
|
found = lesson_booking
|
|
|
|
|
found ||= lesson_session.lesson_booking
|
|
|
|
|
end
|
|
|
|
|
|
2018-02-15 04:16:32 +00:00
|
|
|
def pretty_scheduled_start(with_timezone = true, user_tz = nil)
|
2016-04-06 02:23:15 +00:00
|
|
|
|
|
|
|
|
start_time = scheduled_time(0)
|
|
|
|
|
|
2018-02-15 04:16:32 +00:00
|
|
|
tz_identifier = user_tz || self.timezone
|
|
|
|
|
|
2016-04-06 02:23:15 +00:00
|
|
|
begin
|
2018-02-15 04:16:32 +00:00
|
|
|
tz = TZInfo::Timezone.get(tz_identifier)
|
2016-04-06 02:23:15 +00:00
|
|
|
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
|
2016-05-30 21:43:55 +00:00
|
|
|
"#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{tz.pretty_name})"
|
2016-04-06 02:23:15 +00:00
|
|
|
else
|
|
|
|
|
"#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2016-05-19 18:26:37 +00:00
|
|
|
def pretty_timezone
|
|
|
|
|
begin
|
|
|
|
|
tz = TZInfo::Timezone.get(timezone)
|
2016-05-19 19:12:43 +00:00
|
|
|
tz.pretty_name
|
2016-05-19 18:26:37 +00:00
|
|
|
rescue Exception => e
|
|
|
|
|
@@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
|
2016-05-19 19:12:43 +00:00
|
|
|
'UTC'
|
2016-05-19 18:26:37 +00:00
|
|
|
end
|
|
|
|
|
end
|
2016-05-19 19:12:43 +00:00
|
|
|
|
2018-02-15 04:16:32 +00:00
|
|
|
def pretty_start_time(with_timezone = true, user_tz = nil)
|
2016-04-06 02:23:15 +00:00
|
|
|
|
|
|
|
|
start_time = scheduled_time(0)
|
|
|
|
|
|
2018-02-15 04:16:32 +00:00
|
|
|
tz_identifier = user_tz || self.timezone
|
|
|
|
|
|
2016-04-06 02:23:15 +00:00
|
|
|
begin
|
2018-02-15 04:16:32 +00:00
|
|
|
tz = TZInfo::Timezone.get(tz_identifier)
|
2016-04-06 02:23:15 +00:00
|
|
|
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
|
2016-05-19 19:12:43 +00:00
|
|
|
"#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip} #{tz.pretty_name}"
|
2016-04-06 02:23:15 +00:00
|
|
|
else
|
|
|
|
|
"#{start_time.strftime("%a, %b %e")} at #{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?
|
|
|
|
|
errors.add(:preferred_day, "must be specified")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if slot_type == SLOT_TYPE_RECURRING
|
|
|
|
|
if day_of_week.nil?
|
|
|
|
|
errors.add(:day_of_week, "must be specified")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def validate_slot_minimum_time
|
|
|
|
|
|
|
|
|
|
# this code will fail miserably if the slot is malformed
|
|
|
|
|
if errors.any?
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
minimum_start_time = Time.now
|
|
|
|
|
|
|
|
|
|
if day_of_week
|
|
|
|
|
# this is recurring; it will sort itself out
|
|
|
|
|
else
|
|
|
|
|
time = scheduled_time(0)
|
|
|
|
|
|
|
|
|
|
if time <= minimum_start_time
|
|
|
|
|
#errors.add(:base, "must be at least #{APP_CONFIG.minimum_lesson_booking_hrs} hours in the future")
|
|
|
|
|
errors.add(:preferred_day, "can not be in the past")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|