mirror of
https://github.com/ditkrg/validates_timeliness.git
synced 2026-01-23 06:16:44 +00:00
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:
parent
57c3fdca88
commit
34c0f25225
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user