diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb index a11568f..b52706c 100644 --- a/lib/validates_timeliness/attribute_methods.rb +++ b/lib/validates_timeliness/attribute_methods.rb @@ -1,24 +1,41 @@ +# For Rails 2.0.x: +# This module adds method to create reader method for Time attributes +# to allow for invalid date checking. If date is invalid then returns nil for +# time value. +# +# For Rails >= 2.1 +# This module overrides these AR methods to allow a time value passed to a column +# write method to be stored as is and converts it to a time and caches on read. +# This differs from the normal AR behvaviour where the value is converted and +# cached on write. +# +# This allows the before_type_cast method for the column to return the actual +# value passed to it, treating time columns like all other column types. module ValidatesTimeliness module AttributeMethods def self.included(base) - base.extend ClassMethods + if ActiveRecord::VERSION::STRING < '2.1' + base.extend ClassMethodsOld + else + base.extend ClassMethodsNew + end end - module ClassMethods + # ActiveRecord >= 2.1 + module ClassMethodsNew def define_read_method_for_time_zone_conversion(attr_name) method_body = <<-EOV def #{attr_name}(reload = false) cached = @attributes_cache['#{attr_name}'] return cached if cached && !reload time = read_attribute_before_type_cast('#{attr_name}') - if time && time.acts_like?(:time) - # Rails 2.0.x compatibility check - time = time.respond_to?(:in_time_zone) ? time.in_time_zone : time + if time.acts_like?(:time) + time = time.in_time_zone elsif time - # checks date is valid + # check invalid date time.to_date rescue time = nil - time = time.to_time(:local) if time + time = time.in_time_zone if time end @attributes_cache['#{attr_name}'] = time end @@ -28,19 +45,61 @@ module ValidatesTimeliness def define_write_method_for_time_zone_conversion(attr_name) method_body = <<-EOV - def #{attr_name}=(time) - if time && time.respond_to?(:in_time_zone) - time = time.in_time_zone - elsif time && time.acts_like?(:time) - # Rails 2.0.x compatibility - time = @@default_timezone == :utc ? time.utc : time.localtime + def #{attr_name}=(time) + if time.acts_like?(:time) + time = time.in_time_zone rescue time end write_attribute(:#{attr_name}, time) end EOV evaluate_attribute_method attr_name, method_body, "#{attr_name}=" - end - end - + end + end + + # ActiveRecord < 2.1 + module ClassMethodsOld + def define_attribute_methods + return if generated_methods? + columns_hash.each do |name, column| + unless instance_method_already_implemented?(name) + if self.serialized_attributes[name] + define_read_method_for_serialized_attribute(name) + else + if column.klass == Time + define_read_method_for_time_attribute(name.to_sym) + else + define_read_method(name.to_sym, name, column) + end + end + end + + unless instance_method_already_implemented?("#{name}=") + define_write_method(name.to_sym) + end + + unless instance_method_already_implemented?("#{name}?") + define_question_method(name) + end + end + end + + def define_read_method_for_time_attribute(attr_name) + method_body = <<-EOV + def #{attr_name}(reload = false) + cached = @attributes_cache['#{attr_name}'] + return cached if cached && !reload + time = read_attribute_before_type_cast('#{attr_name}') + unless time.acts_like?(:time) + # check invalid date + time.to_date rescue time = nil + time = time.to_time if time + end + @attributes_cache['#{attr_name}'] = time + end + EOV + evaluate_attribute_method attr_name, method_body + end + end + end end diff --git a/spec/attribute_methods_spec.rb b/spec/attribute_methods_spec.rb index 3527be4..796ee34 100644 --- a/spec/attribute_methods_spec.rb +++ b/spec/attribute_methods_spec.rb @@ -4,18 +4,8 @@ describe ValidatesTimeliness::AttributeMethods do describe "for Time columns" do before do - Person.define_read_method_for_time_zone_conversion(:birth_date_and_time) - Person.define_write_method_for_time_zone_conversion(:birth_date_and_time) @person = Person.new end - - it "should define attribute read method for column" do - @person.respond_to?(:birth_date_and_time).should be_true - end - - it "should define attribute write method for column" do - @person.respond_to?(:birth_date_and_time=).should be_true - end it "should return string value for attribute_before_type_cast when written as string" do @person.birth_date_and_time = "1980-12-25 01:02:03" @@ -30,62 +20,20 @@ describe ValidatesTimeliness::AttributeMethods do it "should return Time object using attribute read method when written with string" do @person.birth_date_and_time = "1980-12-25 01:02:03" @person.birth_date_and_time.should be_kind_of(Time) - end - - if ActiveRecord::VERSION::STRING < '2.1' - it "should return stored time string as Time with correct timezone" do - @person.birth_date_and_time = "1980-12-25 01:02:03" - @person.birth_date_and_time.zone == 'EST' - ActiveRecord::Base.default_timezone = :utc - @person.birth_date_and_time.zone == 'UTC' - end end - + unless ActiveRecord::VERSION::STRING < '2.1' it "should return stored time string as Time with correct timezone" do Time.zone = TimeZone['Sydney'] # no I'm not from Sydney but there is no Melbourne timezone! @person.birth_date_and_time = "1980-12-25 01:02:03" @person.birth_date_and_time.zone == Time.zone end - end + end it "should return nil when time is invalid" do @person.birth_date_and_time = "1980-02-30 01:02:03" @person.birth_date_and_time.should be_nil - end - end - - describe "for Date columns" do - before do - @person = Person.new - end + end - it "should define attribute read method for column" do - @person.respond_to?(:birth_date).should be_true - end - - it "should define attribute write method for column" do - @person.respond_to?(:birth_date=).should be_true - end - - it "should return string value for attribute_before_type_cast when written as string" do - @person.birth_date = "1980-12-25" - @person.birth_date_before_type_cast.should == "1980-12-25" - end - - it "should return Date object for attribute_before_type_cast when written as Date" do - @person.birth_date = date = Date.new(1980, 12, 25) - @person.birth_date_before_type_cast.should be_kind_of(Date) - end - - it "should return Date object using attribute read method when written with string" do - @person.birth_date = "1980-12-25" - @person.birth_date.should be_kind_of(Date) - end - - it "should read stored time with correct timezone" - - it "should return nil when date is invalid" - end - + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e76a1b3..251f6ed 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,11 +3,12 @@ $: << File.dirname(__FILE__) + '/../lib' << File.dirname(__FILE__) require 'rubygems' require 'spec' -if File.exists?(File.dirname(__FILE__) + '/../../../rails') - $: << File.dirname(__FILE__) + '/../../../rails' - require 'activerecord/lib/active_record' - require 'activerecord/lib/active_record/version' - puts "Using vendor ActiveRecord version #{ActiveRecord::VERSION::STRING}" +if File.exists?(File.dirname(__FILE__) + '/../../../../vendor/rails') + $: << File.dirname(__FILE__) + '/../../../../vendor/rails' + require 'activesupport/lib/active_support' + require 'activerecord/lib/active_record' + require 'activerecord/lib/active_record/version' + puts "Using vendored ActiveRecord version #{ActiveRecord::VERSION::STRING}" else require 'active_record' require 'active_record/version' @@ -16,12 +17,7 @@ end require 'validates_timeliness' -conn = { - :adapter => 'sqlite3', - :database => ':memory:' -} - -ActiveRecord::Base.establish_connection(conn) +ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'}) require 'resources/schema' require 'resources/person'