berkmancenter/bookanook

View on GitHub
app/models/reservation.rb

Summary

Maintainability
B
5 hrs
Test Coverage
class Reservation < ActiveRecord::Base
  belongs_to :nook
  belongs_to :requester, class_name: 'User', foreign_key: 'user_id',
    inverse_of: :reservations

  delegate :email, to: :requester

  scope :is_public, -> { where(public: true) }
  scope :confirmed, -> { where(status: Reservation::Status::CONFIRMED) }

  acts_as_taggable_on :remarks

  before_save :ciel_end_time

  alias_attribute :start, :start_time
  alias_attribute :end, :end_time

  module Status
    PENDING, REJECTED, CONFIRMED, CANCELED =
      'Awaiting review', 'Rejected', 'Confirmed', 'Canceled'
    CANCELABLE = [PENDING, CONFIRMED]
    MODIFIABLE = [PENDING]
  end

  STATUSES = Status.constants.map{|s| Status.const_get(s)}.flatten.uniq

  # From active_support/core_ext/numeric/time.rb
  REPEATABLE_UNITS = [
    'second', 'seconds',
    'minute', 'minutes',
    'hour', 'hours',
    'day', 'days',
    'week', 'weeks',
    'fortnight', 'fortnights',
  ]

  serialize :repeats_every

  validates_presence_of :name, :start_time, :end_time, :nook, :requester
  validates_inclusion_of :status, in: STATUSES
  validates_inclusion_of :public, in: [true, false]
  validates_numericality_of :priority, only_integer: true,
    greater_than_or_equal_to: 0
  validate :minimum_length, :maximum_length

  after_initialize :set_defaults

  def time_range
    if start && self.end
      (start.hour * 100 + start.min)...(self.end.hour * 100 + self.end.min)
    end
  end

  def requester_id
    user_id
  end

  def length
    self.end - self.start + 1.second
  end
  alias :duration :length

  def cancel
    self.status = Status::CANCELED
  end

  def confirm
    self.status = Status::CONFIRMED
  end

  def reject
    self.status = Status::REJECTED
  end

  def pending_review?
    status == Status::PENDING
  end

  def confirmed?
    status == Status::CONFIRMED
  end

  def cancelable?
    (Status::CANCELABLE.include? status) && ((self.start.to_i-Time.now.to_i) > (self.nook.modifiable_before*3600))
  end

  def modifiable?
    (Status::MODIFIABLE.include? status) && ((self.start.to_i-Time.now.to_i) > (self.nook.modifiable_before*3600))
  end

  def self.confirmed(reservations=nil)
    return where(status: Status::CONFIRMED) if reservations.nil?
    reservations.where(status: Status::CONFIRMED)
  end

  def self.happening_now
    happening_at(Time.now)
  end

  def self.happening_at(time)
    confirmed.
      where('"reservations"."start_time" < :time AND "reservations"."end_time" > :time',
            { time: time })
  end

  def self.happening_within(time_range, reservations=nil)
    reservations = Reservation.all if reservations.nil?
    reservations.confirmed.where('tsrange("reservations"."start_time", "reservations"."end_time") <@ ' +
                    'tsrange(?, ?)', time_range.begin, time_range.end)
  end

  def self.overlapping_with(time_range)
    confirmed.where('tsrange("reservations"."start_time", "reservations"."end_time") && ' +
                    'tsrange(?, ?)', time_range.begin, time_range.end)
  end

  # Used in downloadable statistics report
  def self.to_csv(reservations, options = {})
    CSV.generate(options) do |csv|
      csv << [ 'Location', 'Nook name', 'Id', 'Name', 'Description', 'Start', 'End', 'Created at' ]
      reservations.each do |reservation|
        csv << [ reservation.nook.location.name,
                 reservation.nook.name,
                 reservation.attributes.values_at('id', 'name', 'description', 'start_time', 'end_time', 'created_at') ].flatten
      end
    end
  end

  # Remind users whose reservations are scheduled on 'date'
  def self.send_reminders(date)
    reservations = Reservation.happening_within(date.beginning_of_day..date.end_of_day)
    for reservation in reservations
      UserMailer.reservation_reminder(reservation.requester, reservation).deliver
    end
  end

  private

  def set_defaults
    self.public ||= true if self.public.nil?
    self.priority ||= 0

    if nook && !nook.requires_approval
      self.status ||= Status::CONFIRMED
    else
      self.status ||= Status::PENDING
    end
  end

  def minimum_length
    if nook && nook.min_reservation_length &&
      duration < nook.min_reservation_length.seconds
      errors.add(:end_time, "can't be less than " +
                 "#{Reservation.humanize_seconds(nook.min_reservation_length)} after start")
    end
  end

  def maximum_length
    if nook && nook.max_reservation_length &&
      duration > nook.max_reservation_length.seconds
      errors.add(:end_time, "can't be more than " +
                 "#{Reservation.humanize_seconds(nook.max_reservation_length)} after start")
    end
  end

  # replaced by reservable_before_hours
  def minimum_start
    if nook && nook.min_schedulable &&
      self.start < Time.now + nook.min_schedulable.seconds
      errors.add(:start_time, "can't start less than " +
                 "#{Reservation.humanize_seconds(nook.min_schedulable)} from now")
    end
  end

  # replaced by unreservable_before_days
  def maximum_start
    if nook && nook.max_schedulable &&
      self.start > Time.now + nook.max_schedulable.seconds
      errors.add(:start_time, "can't start more than " +
                 "#{Reservation.humanize_seconds(nook.max_schedulable)} from now")
    end
  end

  def self.humanize_seconds(secs)
    return '0 hours' if secs == 0
    [[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].map{ |count, name|
      if secs > 0
        secs, n = secs.divmod(count)
        "#{n.to_i} #{name}" unless name == :seconds
      end
    }.compact.reverse.join(' ')
  end

  def ceil_time(time, seconds=60)
    Time.at((time.to_f / seconds).ceil * seconds)
  end

  # ceil to nearest half-hour
  def ciel_end_time
    self.end = ceil_time(self.end, 30.minutes) - 1.seconds
  end
end