dramatically simplify the before_type_cast hack and remove read method override

simplified write method with no dirty attributes hackery
This commit is contained in:
Adam Meehan 2009-06-06 16:37:06 +10:00
parent 57c3fdca88
commit 34c0f25225
3 changed files with 46 additions and 195 deletions

View File

@ -45,6 +45,7 @@ module ValidatesTimeliness
def setup_for_rails
self.default_timezone = ::ActiveRecord::Base.default_timezone
self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
self.enable_active_record_datetime_parser!
load_error_messages
end
end

View File

@ -1,51 +1,31 @@
module ValidatesTimeliness
def self.enable_active_record_datetime_parser!
::ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::AttributeMethods)
end
module ActiveRecord
# Rails 2.1 removed the ability to retrieve the raw value of a time or datetime
# attribute. The raw value is necessary to properly validate a string time or
# datetime value instead of the internal Rails type casting which is very limited
# and does not allow custom formats. These methods restore that ability while
# respecting the automatic timezone handling.
#
# The automatic timezone handling sets the assigned attribute value to the current
# zone in Time.zone. To preserve this localised value and capture the raw value
# we cache the localised value on write and store the raw value in the attributes
# hash for later retrieval and possibly validation. Any value from the database
# will not be in the attribute cache on first read so will be considered in default
# timezone and converted to local time. It is then stored back in the attributes
# hash and cached to avoid the need for any subsequent differentiation.
module AttributeMethods
# Overrides write method for date, time and datetime columns
# to use plugin parser. Also adds mechanism to store value
# before type cast.
#
def self.included(base)
base.extend ClassMethods
base.class_eval do
alias_method_chain :read_attribute, :timeliness
alias_method_chain :reload, :timeliness
alias_method_chain :read_attribute_before_type_cast, :timeliness
class << self
alias_method_chain :define_attribute_methods, :timeliness
end
end
end
# Adds check for cached date/time attributes which have been type cast already
# and value can be used from cache. This prevents the raw date/time value from
# being type cast using default Rails type casting when writing values
# to the database.
def read_attribute_with_timeliness(attr_name)
attr_name = attr_name.to_s
if !(value = @attributes[attr_name]).nil?
column = column_for_attribute(attr_name)
if column && [:date, :time, :datetime].include?(column.type) && @attributes_cache.has_key?(attr_name)
return @attributes_cache[attr_name]
end
end
read_attribute_without_timeliness(attr_name)
end
# If Rails dirty attributes is enabled then the value is added to
# changed attributes if changed. Can't use the default dirty checking
# implementation as it chains the write_attribute method which deletes
# the attribute from the cache.
def write_date_time_attribute(attr_name, value, type, time_zone_aware)
@attributes["_#{attr_name}_before_type_cast"] = value
new = ValidatesTimeliness::Parser.parse(value, type)
if new && type != :date
@ -53,32 +33,17 @@ module ValidatesTimeliness
new = new.in_time_zone if time_zone_aware
end
if defined?(::ActiveRecord::Dirty) && !changed_attributes.include?(attr_name)
old = read_attribute(attr_name)
if old != new
changed_attributes[attr_name] = (old.duplicable? ? old.clone : old)
end
end
@attributes_cache[attr_name] = new
@attributes[attr_name] = value
write_attribute(attr_name.to_sym, new)
end
# If reloading then check if cached, which means its in local time.
# If local, convert with parser as local timezone, otherwise use
# read_attribute method for quick default type cast of values from
# database using default timezone.
def read_date_time_attribute(attr_name, type, time_zone_aware, reload = false)
cached = @attributes_cache[attr_name]
return cached if @attributes_cache.has_key?(attr_name) && !reload
def read_attribute_before_type_cast_with_timeliness(attr_name)
return @attributes["_#{attr_name}_before_type_cast"] if @attributes.has_key?("_#{attr_name}_before_type_cast")
read_attribute_before_type_cast_without_timeliness(attr_name)
end
if @attributes_cache.has_key?(attr_name)
time = read_attribute_before_type_cast(attr_name)
time = ValidatesTimeliness::Parser.parse(time, type)
else
time = read_attribute(attr_name)
@attributes[attr_name] = (time && time_zone_aware ? time.in_time_zone : time) unless frozen?
end
@attributes_cache[attr_name] = time && time_zone_aware ? time.in_time_zone : time
def reload_with_timeliness
@attributes.keys.grep(/^_.*_before_type_cast$/).each { |key| @attributes.delete(key) }
reload_without_timeliness
end
module ClassMethods
@ -86,46 +51,24 @@ module ValidatesTimeliness
def define_attribute_methods_with_timeliness
return if generated_methods?
columns_hash.each do |name, column|
unless instance_method_already_implemented?(name)
if [:date, :time, :datetime].include?(column.type)
time_zone_aware = create_time_zone_conversion_attribute?(name, column) rescue false
define_read_method_for_dates_and_times(name, column.type, time_zone_aware)
end
if [:date, :time, :datetime].include?(column.type)
time_zone_aware = create_time_zone_conversion_attribute?(name, column) rescue false
class_eval <<-EOV
def #{name}=(value)
write_date_time_attribute('#{name}', value, #{column.type.inspect}, #{time_zone_aware})
end
EOV
end
unless instance_method_already_implemented?("#{name}=")
if [:date, :time, :datetime].include?(column.type)
time_zone_aware = create_time_zone_conversion_attribute?(name, column) rescue false
define_write_method_for_dates_and_times(name, column.type, time_zone_aware)
end
end
end
define_attribute_methods_without_timeliness
end
def define_write_method_for_dates_and_times(attr_name, type, time_zone_aware)
method_body = <<-EOV
def #{attr_name}=(value)
write_date_time_attribute('#{attr_name}', value, #{type.inspect}, #{time_zone_aware})
end
EOV
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
end
def define_read_method_for_dates_and_times(attr_name, type, time_zone_aware)
method_body = <<-EOV
def #{attr_name}(reload = false)
read_date_time_attribute('#{attr_name}', #{type.inspect}, #{time_zone_aware}, reload)
end
EOV
evaluate_attribute_method attr_name, method_body
end
end
end
end
end
ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::AttributeMethods)

View File

@ -20,24 +20,6 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date_and_time = "2000-01-01 12:00"
end
it "should call read_date_time_attribute when date attribute is retrieved" do
@person.should_receive(:read_date_time_attribute)
@person.birth_date = "2000-01-01"
@person.birth_date
end
it "should call read_date_time_attribute when time attribute is retrieved" do
@person.should_receive(:read_date_time_attribute)
@person.birth_time = "12:00"
@person.birth_time
end
it "should call read_date_time_attribute when datetime attribute is retrieved" do
@person.should_receive(:read_date_time_attribute)
@person.birth_date_and_time = "2000-01-01 12:00"
@person.birth_date_and_time
end
it "should call parser on write for datetime attribute" do
ValidatesTimeliness::Parser.should_receive(:parse).once
@person.birth_date_and_time = "2000-01-01 02:03:04"
@ -103,7 +85,20 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date_and_time_before_type_cast.should be_nil
end
unless RAILS_VER < '2.1'
if RAILS_VER < '2.1'
it "should return time object from database in default timezone" do
ActiveRecord::Base.default_timezone = :utc
time_string = "2000-01-01 09:00:00"
@person = Person.new
@person.birth_date_and_time = time_string
@person.save
@person.reload
@person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z').should == time_string + ' GMT'
end
else
it "should return stored time string as Time with correct timezone" do
Time.zone = 'Melbourne'
time_string = "2000-06-01 02:03:04"
@ -121,84 +116,6 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z %z').should == time_string + ' EST +1000'
end
describe "dirty attributes" do
it "should return true for attribute changed? when value updated" do
time_string = "2000-01-01 02:03:04"
@person.birth_date_and_time = time_string
@person.birth_date_and_time_changed?.should be_true
end
it "should show changes when time attribute changed from nil to Time object" do
time_string = "2000-01-01 02:03:04"
@person.birth_date_and_time = time_string
time = @person.birth_date_and_time
@person.changes.should == {"birth_date_and_time" => [nil, time]}
end
it "should show changes when time attribute changed from Time object to nil" do
time_string = "2020-01-01 02:03:04"
@person.birth_date_and_time = time_string
@person.save false
@person.reload
time = @person.birth_date_and_time
@person.birth_date_and_time = nil
@person.changes.should == {"birth_date_and_time" => [time, nil]}
end
it "should show no changes when assigned same value as Time object" do
time_string = "2020-01-01 02:03:04"
@person.birth_date_and_time = time_string
@person.save false
@person.reload
time = @person.birth_date_and_time
@person.birth_date_and_time = time
@person.changes.should == {}
end
it "should show no changes when assigned same value as time string" do
time_string = "2020-01-01 02:03:04"
@person.birth_date_and_time = time_string
@person.save false
@person.reload
@person.birth_date_and_time = time_string
@person.changes.should == {}
end
end
else
it "should return time object from database in default timezone" do
ActiveRecord::Base.default_timezone = :utc
time_string = "2000-01-01 09:00:00"
@person = Person.new
@person.birth_date_and_time = time_string
@person.save
@person.reload
@person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z').should == time_string + ' GMT'
end
end
it "should return same time object on repeat reads on existing object" do
Time.zone = 'Melbourne' unless RAILS_VER < '2.1'
time_string = "2000-01-01 09:00:00"
@person = Person.new
@person.birth_date_and_time = time_string
@person.save!
@person.reload
time = @person.birth_date_and_time
@person.birth_date_and_time.should == time
end
it "should return same date object on repeat reads on existing object" do
date_string = Date.today
@person = Person.new
@person.birth_date = date_string
@person.save!
@person.reload
date = @person.birth_date
@person.birth_date.should == date
end
it "should return correct date value after new value assigned" do
@ -220,15 +137,5 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.reload
@person.birth_date.should == tomorrow
end
it "should skip storing value in attributes hash on read if record frozen" do
@person = Person.new
@person.birth_date = Date.today
@person.save!
@person.reload
@person.freeze
@person.frozen?.should be_true
lambda { @person.birth_date }.should_not raise_error
@person.birth_date.should == Date.today
end
end