Skip to content

Instantly share code, notes, and snippets.

@tsnow
Created May 20, 2011 23:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tsnow/984025 to your computer and use it in GitHub Desktop.
Save tsnow/984025 to your computer and use it in GitHub Desktop.
For stackoverflow Q#4955355
# This presumes that each day is seperate, and you don't actually want to expose
# the durations to the user, ie, for "take this week off"
# (thus the goofy name HoliDay which emphasizes the day-nature,)
# and also that the HTML-interface you want is checkboxes for :am_pm and :all_day,
# and :date as a normal rails-style date select list.
#app/models/holi_day.rb
# The actual model logic that extends the general concept of a
# holiday into one which only handles single-day requests.
require 'holiday'
class HoliDay < Holiday
# This is ever so slightly a hack.
# We're going to use AR::Base's STI facility to transparently
# decorate these objects. There's no actual :type field in the db.
# The same functionality could be done with normal decorators as well.
# or even down in the Presenter class, as private methods.
#This is all model-db-level info, the view shouldn't know about this stuff:
ALL_DAY = [0.hours, 24.hours]
MORNING_EVENING = {
false => [9.hours,12.hours], #:pm? #=> false
true => [12.hours,17.hours] #:pm? #=> true
}
#Just building a queryable data structure here for our args mapping the booleans
# from the view to ranges of time in the db
ALL_DAY_MORNING_EVENING = {
true => {true => ALL_DAY, false => ALL_DAY},
false => MORNING_EVENING
}
#Little bridge for the view-attributes
def pm?
start_time && (start_time.hour >= 12) #This should be equal to at least EVENING start, if not MORNING end, above.
end
def date
start_time && start_time.to_date
end
def normalize_holi_day_attrs(unchecked_args)
args = {:date => self.date || Date.today, :am_pm => true, :all_day => true}.merge(unchecked_args)
#am_pm and all_day need to be boolean, and we assume they're true by default
#date must be present
args
end
#Knows how to change the view-style data into the db-style range type
#Wat up MTV: Dis my data representation bridge, dis where da magic happen
def holi_day_attrs_to_duration(args={:date=>Date.today, :am_pm => true, :all_day => true})
args = normalize_holi_day_attrs(args)
days = [args[:date].to_time,args[:date].to_time] #=>[Date.today, Date.today]
times = ALL_DAY_MORNING_EVENING[args[:all_day]][args[:am_pm]] #=>[9.hours, 12.hours]
a_duration = days.zip(times).map{|day,time| day+time} #=> [[Date.today, 9.hours],[Date.today, 12.hours]] #=> [today at 9, today at noon]
Range.new(*a_duration)
end
def duration=(arg)
if arg.is_a?(Hash)
self.duration = holi_day_attrs_to_duration(arg)
else
super(arg)
end
end
end
#app/helpers/holi_day_presenter.rb
require 'forwardable'
require 'holi_day'
class HoliDayPresenter
# A small proxy for the view that mediates between the view-tag-helpers and the model.
# You can use it like so in the controller:
# @holi_day = HoliDayPresenter.new() #quacks like a HoliDay as far as active_model goes.
# Or:
# @holi_day = HoliDayPresenter.new(HoliDay.find(params[:id])) #Though, it would be easy to make
# #HoliDayPresenter implement a #find
# #class_method, if you care.
# @holi_day.update_attributes(params[:holi_day])
# @holi_day.valid? && @holi_day.save
# And the view _only_ knows the HoliDay via the presenter:
# <% semantic_form_for :holi_day, :object => @holi_day do |f| %>
# <%= f.input :am_pm %> <%#because these act like t/f attributes to the view %>
# <%= f.input :all_day %>
# <%= f.input :date %>
# <%= f.commit_button %>
# <% end %>
extend Forwardable
def initialize(atts={})
@model= case atts
when HoliDay then
atts
when Hash then
HoliDay.new(self.durationize_atts(atts))
else
HoliDay.new
end
end
#view-facing methods
def_delegator :@model, :pm?, :am_pm #makes :am_pm act like a t/f attribute
def_delegator :@model, :date
def_delegator :@model, :all_day?, :all_day #makes :all_day act like a t/f attribute
#Standard AR::Model bits here for error messages
def_delegator :@model, :valid?
def_delegator :@model, :save
def durationize_atts(att)
att = att.clone
others = att.slice!(:date, :all_day, :am_pm)
others.merge!({:duration => att}) #subtle: needs to exist so records initialize properly.
others
end
def update_attributes(att={})
@model.update_attributes(self.durationize_atts(att))
end
end
gem 'activerecord', '> 2.3.8','< 3.1'
gem 'activesupport', '> 2.3.8', '< 3.1'
require 'test/unit'
require 'active_record'
require 'active_support'
require 'forwardable'
ActiveRecord::Base.establish_connection({
'adapter' => 'sqlite3',
'database' => 'db/holidays',
'timeout' => 5000
})
ActiveRecord::Migration.create_table 'holidays', :force => true do |h| #drops on create
h.datetime :start_time
h.datetime :finish_time
h.timestamps
end
$LOAD_PATH.push(File.dirname(__FILE__)) unless RUBY_VERSION < '1.9'
#test/unit/holi_day_test.rb
require 'active_support/core_ext/kernel/requires'
require 'active_support/test_case'
require 'active_record/test_case'
class HoliDayTest < Test::Unit::TestCase
extend ActiveSupport::Testing::Declarative
require 'holiday'
#Holiday
test "a holiday has a start_time and finish_time duration" do
t = Time.current
h=Holiday.new(:start_time => t, :finish_time => 4.days.from_now(t))
assert_equal 4.days, h.duration.last - h.duration.first
end
test "updating a holiday duration" do
h=Holiday.new
t=Time.current
h.duration = Range.new(t, 4.days.from_now(t))
assert_equal 4.days, h.finish_time - h.start_time
end
require 'holi_day'
#HoliDay
test "an all_day holi_day from the db's pov" do
h=HoliDay.new(:start_time => Date.today, :finish_time => 24.hours.from_now(Date.today))
assert_equal [true], [h.all_day?]
end
test "a morning holi_day from the db's pov" do
h=HoliDay.new(:start_time => 9.hours.from_now(Date.today), :finish_time => 12.hours.from_now(Date.today))
assert_equal [false,false, Date.today], [h.all_day?, h.pm?, h.date]
end
test "an evening holi_day from the db's pov" do
h=HoliDay.new(:start_time => 12.hours.from_now(Date.today), :finish_time => 24.hours.from_now(Date.today))
assert_equal [false,true, Date.today], [h.all_day?, h.pm?, h.date]
end
test "handles presenter attr hash for duration" do
h=HoliDay.new(:start_time => Date.today, :finish_time => 24.hours.from_now(Date.today))
yr = 1.year.from_now(Date.today)
h.duration = {:date => yr, :am_pm => true, :all_day => true}
assert_equal [true,false, yr.to_time - Date.today.to_time ], [h.all_day?, h.pm?, (h.date.to_time - Date.today.to_time) ]
end
require 'holi_day_presenter'
#HoliDayPresenter
#db hit
test "the presenter passes on relevant methods" do
h=HoliDay.new(:start_time => 12.hours.from_now(Date.today), :finish_time => 24.hours.from_now(Date.today))
h_p = HoliDayPresenter.new(h)
assert_equal [false,true, Date.today, true, true], [h_p.all_day, h_p.am_pm, h_p.date, h_p.valid?, h_p.save]
end
#db hit
test "update_attributes handles other args as well as duration" do #This might need the other attrs' getters implemented as well.
h=HoliDay.new
h_p = HoliDayPresenter.new(h)
h_p.update_attributes(:am_pm => false, :all_day => false,
:created_at => 2.minutes.ago(Time.current))
assert_equal [2, false,false, Date.today, true, true ], [Time.current.min - h.created_at.min, h_p.all_day, h_p.am_pm, h_p.date, h_p.valid?, h_p.save]
end
test "on today and all_day, is the default" do
h_p = HoliDayPresenter.new
assert_equal [true,false, Date.today], [h_p.all_day, h_p.am_pm, h_p.date]
end
end
#app/models/holiday.rb
class Holiday < ActiveRecord::Base
composed_of :duration, :mapping =>[[:start_time, :first],[:finish_time,:last]], :class_name => 'Range'
#note that this will work for days-long durations, db-wise.
def all_day?
finish_time && start_time && ((finish_time.to_time - start_time.to_time ).to_i % 1.day.to_i == 0)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment