# 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" belongs_to :proposer, class_name: "JamRuby::User", inverse_of: :proposed_slots, foreign_key: :proposer_id 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 def is_recurring? slot_type == SLOT_TYPE_RECURRING end 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 # 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 def scheduled_times(needed_sessions, minimum_start_time) #puts "NEEDED SESSIONS #{needed_sessions} #{minimum_start_time}" times = [] week_offset = 0 needed_sessions.times do |i| 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 candidate = scheduled_time(i + week_offset) #puts "retry #1 #{candidate}" # sanity check if candidate <= minimum_start_time week_offset += 1 candidate = scheduled_time(i + week_offset) #puts "retry #2 #{candidate}" if candidate <= minimum_start_time 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 end 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 def pretty_scheduled_start(with_timezone = true, user_tz = nil) start_time = scheduled_time(0) tz_identifier = user_tz || self.timezone begin tz = TZInfo::Timezone.get(tz_identifier) 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.pretty_name})" else "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" end end def pretty_timezone begin tz = TZInfo::Timezone.get(timezone) tz.pretty_name rescue Exception => e @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") 'UTC' end end def pretty_start_time(with_timezone = true, user_tz = nil) start_time = scheduled_time(0) tz_identifier = user_tz || self.timezone begin tz = TZInfo::Timezone.get(tz_identifier) 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")} at #{start_time.strftime("%l:%M%P").strip} #{tz.pretty_name}" 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