mirror of
https://github.com/ditkrg/validates_timeliness.git
synced 2026-01-25 07:16:41 +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
|
def setup_for_rails
|
||||||
self.default_timezone = ::ActiveRecord::Base.default_timezone
|
self.default_timezone = ::ActiveRecord::Base.default_timezone
|
||||||
self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
|
self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
|
||||||
|
self.enable_active_record_datetime_parser!
|
||||||
load_error_messages
|
load_error_messages
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,51 +1,31 @@
|
|||||||
module ValidatesTimeliness
|
module ValidatesTimeliness
|
||||||
|
|
||||||
|
def self.enable_active_record_datetime_parser!
|
||||||
|
::ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::AttributeMethods)
|
||||||
|
end
|
||||||
|
|
||||||
module ActiveRecord
|
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
|
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)
|
def self.included(base)
|
||||||
base.extend ClassMethods
|
base.extend ClassMethods
|
||||||
base.class_eval do
|
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
|
class << self
|
||||||
alias_method_chain :define_attribute_methods, :timeliness
|
alias_method_chain :define_attribute_methods, :timeliness
|
||||||
end
|
end
|
||||||
end
|
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)
|
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)
|
new = ValidatesTimeliness::Parser.parse(value, type)
|
||||||
|
|
||||||
if new && type != :date
|
if new && type != :date
|
||||||
@ -53,32 +33,17 @@ module ValidatesTimeliness
|
|||||||
new = new.in_time_zone if time_zone_aware
|
new = new.in_time_zone if time_zone_aware
|
||||||
end
|
end
|
||||||
|
|
||||||
if defined?(::ActiveRecord::Dirty) && !changed_attributes.include?(attr_name)
|
write_attribute(attr_name.to_sym, new)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# If reloading then check if cached, which means its in local time.
|
def read_attribute_before_type_cast_with_timeliness(attr_name)
|
||||||
# If local, convert with parser as local timezone, otherwise use
|
return @attributes["_#{attr_name}_before_type_cast"] if @attributes.has_key?("_#{attr_name}_before_type_cast")
|
||||||
# read_attribute method for quick default type cast of values from
|
read_attribute_before_type_cast_without_timeliness(attr_name)
|
||||||
# database using default timezone.
|
end
|
||||||
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
|
|
||||||
|
|
||||||
if @attributes_cache.has_key?(attr_name)
|
def reload_with_timeliness
|
||||||
time = read_attribute_before_type_cast(attr_name)
|
@attributes.keys.grep(/^_.*_before_type_cast$/).each { |key| @attributes.delete(key) }
|
||||||
time = ValidatesTimeliness::Parser.parse(time, type)
|
reload_without_timeliness
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
@ -86,46 +51,24 @@ module ValidatesTimeliness
|
|||||||
def define_attribute_methods_with_timeliness
|
def define_attribute_methods_with_timeliness
|
||||||
return if generated_methods?
|
return if generated_methods?
|
||||||
columns_hash.each do |name, column|
|
columns_hash.each do |name, column|
|
||||||
unless instance_method_already_implemented?(name)
|
|
||||||
if [:date, :time, :datetime].include?(column.type)
|
if [:date, :time, :datetime].include?(column.type)
|
||||||
time_zone_aware = create_time_zone_conversion_attribute?(name, column) rescue false
|
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
|
class_eval <<-EOV
|
||||||
|
def #{name}=(value)
|
||||||
|
write_date_time_attribute('#{name}', value, #{column.type.inspect}, #{time_zone_aware})
|
||||||
|
end
|
||||||
|
EOV
|
||||||
end
|
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
|
end
|
||||||
define_attribute_methods_without_timeliness
|
define_attribute_methods_without_timeliness
|
||||||
end
|
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
|
||||||
|
|
||||||
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"
|
@person.birth_date_and_time = "2000-01-01 12:00"
|
||||||
end
|
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
|
it "should call parser on write for datetime attribute" do
|
||||||
ValidatesTimeliness::Parser.should_receive(:parse).once
|
ValidatesTimeliness::Parser.should_receive(:parse).once
|
||||||
@person.birth_date_and_time = "2000-01-01 02:03:04"
|
@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
|
@person.birth_date_and_time_before_type_cast.should be_nil
|
||||||
end
|
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
|
it "should return stored time string as Time with correct timezone" do
|
||||||
Time.zone = 'Melbourne'
|
Time.zone = 'Melbourne'
|
||||||
time_string = "2000-06-01 02:03:04"
|
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'
|
@person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z %z').should == time_string + ' EST +1000'
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
it "should return correct date value after new value assigned" do
|
it "should return correct date value after new value assigned" do
|
||||||
@ -221,14 +138,4 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
|
|||||||
@person.birth_date.should == tomorrow
|
@person.birth_date.should == tomorrow
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user