From 0ad8ace3350435b8320732a2a9a5459fc7c36c4c Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 13 Jan 2009 10:12:41 +1100 Subject: [PATCH] refactored AR attribute methods to define read method for all date, time and datetime attributes. Makes things much clearer and fixes bug reported (#2) by Brad (pvjg) --- .../active_record/attribute_methods.rb | 75 ++++++++----------- spec/active_record/attribute_methods_spec.rb | 18 +++++ 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/lib/validates_timeliness/active_record/attribute_methods.rb b/lib/validates_timeliness/active_record/attribute_methods.rb index 8eaf99d..393591a 100644 --- a/lib/validates_timeliness/active_record/attribute_methods.rb +++ b/lib/validates_timeliness/active_record/attribute_methods.rb @@ -14,24 +14,10 @@ module ValidatesTimeliness # 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. - # - # The wholesale replacement of the Rails time type casting is not done to - # preserve the quickest conversion for timestamp columns and also any value - # which is never changed during the life of the record object. module AttributeMethods def self.included(base) base.extend ClassMethods - - if Rails::VERSION::STRING < '2.1' - base.class_eval do - class << self - def create_time_zone_conversion_attribute?(name, column) - false - end - end - end - end end # Adds check for cached date/time attributes which have been type cast already @@ -57,25 +43,19 @@ module ValidatesTimeliness end end - # Writes attribute value by storing raw value in attributes hash, - # then convert it with parser and cache it. - # # 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) - column = column_for_attribute(attr_name) + def write_date_time_attribute(attr_name, value, type, time_zone_aware) old = read_attribute(attr_name) if defined?(::ActiveRecord::Dirty) - new = self.class.parse_date_time(value, column.type) + new = self.class.parse_date_time(value, type) - unless column.type == :date || new.nil? + unless type == :date || new.nil? new = new.to_time rescue new end - if self.class.send(:create_time_zone_conversion_attribute?, attr_name, column) - new = new.in_time_zone rescue nil - end + new = new.in_time_zone if new && time_zone_aware @attributes_cache[attr_name] = new if defined?(::ActiveRecord::Dirty) && !changed_attributes.include?(attr_name) && old != new @@ -84,6 +64,24 @@ module ValidatesTimeliness @attributes[attr_name] = value 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 + + if @attributes_cache.has_key?(attr_name) + time = read_attribute_before_type_cast(attr_name) + time = self.class.parse_date_time(date, type) + else + time = read_attribute(attr_name) + @attributes[attr_name] = time && time_zone_aware ? time.in_time_zone : time + end + @attributes_cache[attr_name] = time && time_zone_aware ? time.in_time_zone : time + end + module ClassMethods # Override AR method to define attribute reader and writer method for @@ -94,8 +92,9 @@ module ValidatesTimeliness unless instance_method_already_implemented?(name) if self.serialized_attributes[name] define_read_method_for_serialized_attribute(name) - elsif create_time_zone_conversion_attribute?(name, column) - define_read_method_for_time_zone_conversion(name) + elsif [: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) else define_read_method(name.to_sym, name, column) end @@ -103,7 +102,8 @@ module ValidatesTimeliness unless instance_method_already_implemented?("#{name}=") if [:date, :time, :datetime].include?(column.type) - define_write_method_for_dates_and_times(name) + 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) else define_write_method(name.to_sym) end @@ -116,32 +116,19 @@ module ValidatesTimeliness end # Define write method for date, time and datetime columns - def define_write_method_for_dates_and_times(attr_name) + 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) + write_date_time_attribute('#{attr_name}', value, #{type.inspect}, #{time_zone_aware}) end EOV evaluate_attribute_method attr_name, method_body, "#{attr_name}=" end - # Define time attribute reader. 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 define_read_method_for_time_zone_conversion(attr_name) + def define_read_method_for_dates_and_times(attr_name, type, time_zone_aware) method_body = <<-EOV def #{attr_name}(reload = false) - cached = @attributes_cache['#{attr_name}'] - return cached if @attributes_cache.has_key?('#{attr_name}') && !reload - if @attributes_cache.has_key?('#{attr_name}') - time = read_attribute_before_type_cast('#{attr_name}') - time = self.class.parse_date_time(date, :datetime) - else - time = read_attribute('#{attr_name}') - @attributes['#{attr_name}'] = time.in_time_zone rescue nil - end - @attributes_cache['#{attr_name}'] = time.in_time_zone rescue nil + read_date_time_attribute('#{attr_name}', #{type.inspect}, #{time_zone_aware}, reload) end EOV evaluate_attribute_method attr_name, method_body diff --git a/spec/active_record/attribute_methods_spec.rb b/spec/active_record/attribute_methods_spec.rb index e13b102..2139634 100644 --- a/spec/active_record/attribute_methods_spec.rb +++ b/spec/active_record/attribute_methods_spec.rb @@ -23,6 +23,24 @@ 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 rea_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 @person.class.should_receive(:parse_date_time).once @person.birth_date_and_time = "2000-01-01 02:03:04"