From 800b187d080aaa306fe3e0ca5bcfe187f90e700d Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 22 Jul 2008 10:11:27 +1000 Subject: [PATCH 1/6] added Date class define write method convert with parser removed strict_time_type_cast method and use parse method directly --- lib/validates_timeliness/attribute_methods.rb | 132 +++++++++--------- 1 file changed, 64 insertions(+), 68 deletions(-) diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb index 31ca783..c535883 100644 --- a/lib/validates_timeliness/attribute_methods.rb +++ b/lib/validates_timeliness/attribute_methods.rb @@ -11,32 +11,21 @@ module ValidatesTimeliness # To do this we must cache the user entered values 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 UTC time and then converted to local time. + # 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 quick conversion for timestamp columns and also any value which - # is never touched during the life of the record object. + # is never changed during the life of the record object. + # + # Dates are also handled but only write to cache value converted by plugin + # parser. Default read method will retrieve from cache or do default + # conversion module AttributeMethods def self.included(base) base.extend ClassMethods - if Rails::VERSION::STRING < '2.1' - base.class_eval do - class << self - alias_method :define_read_method_for_time, :define_read_method_for_time_zone_conversion - alias_method :define_write_method_for_time, :define_write_method_for_time_zone_conversion - end - end - base.extend ClassMethodsOld - end - end - - # Does strict time type cast checking for Rails 2.1 timezone handling - def strict_time_type_cast(time) - time = self.class.parse_date_time(time, :datetime) - time_in_time_zone(time) end # Handles timezone shift if Rails 2.1 @@ -53,7 +42,7 @@ module ValidatesTimeliness if column = column_for_attribute(attr_name) if unserializable_attribute?(attr_name, column) unserialize_attribute(attr_name) - elsif column.klass == Time && @attributes_cache.has_key?(attr_name) + elsif column.klass == Time && @attributes_cache.has_key?(attr_name) @attributes_cache[attr_name] else column.type_cast(value) @@ -67,9 +56,40 @@ module ValidatesTimeliness end module ClassMethods + + # Modified from AR to define Date and Time attribute reader and writer + # methods with strict time type casting. + 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) + elsif column.klass == Time + define_read_method_for_time_zone_conversion(name.to_sym) + else + define_read_method(name.to_sym, name, column) + end + end + + unless instance_method_already_implemented?("#{name}=") + if column.klass == Time + define_write_method_for_time_zone_conversion(name.to_sym) + elsif column.klass == Date + define_write_method_for_date(name.to_sym) + else + define_write_method(name.to_sym) + end + end + + unless instance_method_already_implemented?("#{name}?") + define_question_method(name) + end + end + end - # Define time attribute write method to store time value as is without - # conversion and then convert time with strict conversion and cache it. + # Define time attribute write method to store raw time value in + # attributes hash, then convert it with parser and cache it. # # If Rails 2.1 dirty attributes is enabled then the value is added to # changed attributes if changed. Can't use the default dirty checking @@ -81,7 +101,7 @@ module ValidatesTimeliness old = read_attribute('#{attr_name}') if defined?(ActiveRecord::Dirty) @attributes['#{attr_name}'] = time unless time.acts_like?(:time) - time = strict_time_type_cast(time) + time = self.class.parse_date_time(time, :datetime) end time = time_in_time_zone(time) if defined?(ActiveRecord::Dirty) && !changed_attributes.include?('#{attr_name}') && old != time @@ -94,62 +114,38 @@ module ValidatesTimeliness end # Define time attribute reader. If reloading then check if cached, - # which means its in local time. If local, do strict type cast as local + # 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) - 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 = strict_time_type_cast(time) - else - time = read_attribute('#{attr_name}') - @attributes['#{attr_name}'] = time_in_time_zone(time) + 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(time) + end + @attributes_cache['#{attr_name}'] = time_in_time_zone(time) + end + EOV + evaluate_attribute_method attr_name, method_body + end + + def define_write_method_for_date(attr_name) + method_body = <<-EOV + def #{attr_name}=(date) + @attributes_cache['#{attr_name}'] ||= self.class.parse_date_time(date, :date) + @attributes['#{attr_name}'] = date end - @attributes_cache['#{attr_name}'] = time_in_time_zone(time) - end EOV evaluate_attribute_method attr_name, method_body end end - - # Only for Rails 2.0.x. Checks for time attributes to define special reader - # and writer methods. - module ClassMethodsOld - - # Modified from AR to define Time attribute reader and writer methods with - # strict time type casting. - 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) - elsif column.klass == Time - define_read_method_for_time(name.to_sym) - else - define_read_method(name.to_sym, name, column) - end - end - - unless instance_method_already_implemented?("#{name}=") - if column.klass == Time - define_write_method_for_time(name.to_sym) - else - define_write_method(name.to_sym) - end - end - - unless instance_method_already_implemented?("#{name}?") - define_question_method(name) - end - end - end - end end end From 175b5c8d3684a0e35c66b6fdaf7c372508e90ef5 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 22 Jul 2008 10:12:53 +1000 Subject: [PATCH 2/6] moved type cast conversion specs from attribute_methods to validations --- spec/attribute_methods_spec.rb | 28 +------------------------ spec/validations_spec.rb | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/spec/attribute_methods_spec.rb b/spec/attribute_methods_spec.rb index 59504f1..8d42965 100644 --- a/spec/attribute_methods_spec.rb +++ b/spec/attribute_methods_spec.rb @@ -7,33 +7,7 @@ describe ValidatesTimeliness::AttributeMethods do before do @person = Person.new end - - describe "strict_time_type_cast" do - it "should return time object for valid time string" do - strict_time_type_cast("2000-01-01 12:13:14").should be_kind_of(Time) - end - - it "should return nil for time string with invalid date part" do - strict_time_type_cast("2000-02-30 12:13:14").should be_nil - end - - it "should return nil for time string with invalid time part" do - strict_time_type_cast("2000-02-01 25:13:14").should be_nil - end - - it "should return Time object when passed a Time object" do - strict_time_type_cast(Time.now).should be_kind_of(Time) - end - - if RAILS_VER >= '2.1' - it "should convert time string into current timezone" do - Time.zone = 'Melbourne' - time = strict_time_type_cast("2000-01-01 12:13:14") - Time.zone.utc_offset.should == 10.hours - end - end - end - + it "should return string value for attribute_before_type_cast when written as string" do time_string = "2000-06-01 01:02:03" @person.birth_date_and_time = time_string diff --git a/spec/validations_spec.rb b/spec/validations_spec.rb index e3ed71c..55a78d8 100644 --- a/spec/validations_spec.rb +++ b/spec/validations_spec.rb @@ -10,6 +10,44 @@ describe ValidatesTimeliness::Validations do Time.now = nil end + describe "timeliness_date_time_parse" do + it "should return time object for valid time string" do + parse_method("2000-01-01 12:13:14", :datetime).should be_kind_of(Time) + end + + it "should return nil for time string with invalid date part" do + parse_method("2000-02-30 12:13:14", :datetime).should be_nil + end + + it "should return nil for time string with invalid time part" do + parse_method("2000-02-01 25:13:14", :datetime).should be_nil + end + + it "should return Time object when passed a Time object" do + parse_method(Time.now, :datetime).should be_kind_of(Time) + end + + if RAILS_VER >= '2.1' + it "should convert time string into current timezone" do + Time.zone = 'Melbourne' + time = parse_method("2000-01-01 12:13:14", :datetime) + Time.zone.utc_offset.should == 10.hours + end + end + + it "should return Date object valid date string" do + parse_method("2000-02-01", :date).should be_kind_of(Date) + end + + it "should return nil for invalid date string" do + parse_method("2000-02-30", :date).should be_nil + end + + def parse_method(*args) + ActiveRecord::Base.timeliness_date_time_parse(*args) + end + end + describe "with no restrictions" do before :all do class BasicValidation < Person From a674089c00bfb31a7a08c23f988af9c9e73650b8 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 22 Jul 2008 10:13:52 +1000 Subject: [PATCH 3/6] return Date object for date type timeliness_date_time_parse method --- lib/validates_timeliness/validations.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/validates_timeliness/validations.rb b/lib/validates_timeliness/validations.rb index 491ce93..d003c46 100644 --- a/lib/validates_timeliness/validations.rb +++ b/lib/validates_timeliness/validations.rb @@ -25,11 +25,10 @@ module ValidatesTimeliness # Override this method to use any date parsing algorithm you like such as # Chronic. Just return nil for an invalid value and a Time object for a # valid parsed value. - # def timeliness_date_time_parse(raw_value, type, strict=true) return raw_value.to_time if raw_value.acts_like?(:time) || raw_value.is_a?(Date) - time_array = ValidatesTimeliness::Formats.extract_date_time_values(raw_value, type, strict) + time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict) raise if time_array.nil? if type == :time @@ -38,7 +37,8 @@ module ValidatesTimeliness end # Date.new enforces days per month, unlike Time - Date.new(*time_array[0..2]) unless type == :time + date = Date.new(*time_array[0..2]) unless type == :time + return date if type == :date # Check time part, and return time object Time.local(*time_array) rescue DateTime.new(*time_array[0..5]) From 8082b5ce1cb1f14e6afd9623c3f0d0c266462230 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 22 Jul 2008 11:39:17 +1000 Subject: [PATCH 4/6] added specs for checking call to parse method to verify read and write attribute methods are defined --- spec/attribute_methods_spec.rb | 41 ++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/spec/attribute_methods_spec.rb b/spec/attribute_methods_spec.rb index 8d42965..7afe2b0 100644 --- a/spec/attribute_methods_spec.rb +++ b/spec/attribute_methods_spec.rb @@ -8,29 +8,39 @@ describe ValidatesTimeliness::AttributeMethods do @person = Person.new end - it "should return string value for attribute_before_type_cast when written as string" do - time_string = "2000-06-01 01:02:03" + it "should call parser on write for time attribute" do + @person.class.should_receive(:parse_date_time).once + @person.birth_date_and_time = "2000-06-01 02:03:04" + end + + it "should call parser on write for date attribute" do + @person.class.should_receive(:parse_date_time).once + @person.birth_date = "2000-06-01" + end + + it "should return raw string value for attribute_before_type_cast when written as string" do + time_string = "2000-06-01 02:03:04" @person.birth_date_and_time = time_string @person.birth_date_and_time_before_type_cast.should == time_string end it "should return Time object for attribute_before_type_cast when written as Time" do - @person.birth_date_and_time = Time.mktime(2000, 06, 01, 1, 2, 3) + @person.birth_date_and_time = Time.mktime(2000, 6, 1, 2, 3, 4) @person.birth_date_and_time_before_type_cast.should be_kind_of(Time) end it "should return Time object using attribute read method when written with string" do - @person.birth_date_and_time = "2000-06-01 01:02:03" + @person.birth_date_and_time = "2000-06-01 02:03:04" @person.birth_date_and_time.should be_kind_of(Time) end it "should return nil when time is invalid" do - @person.birth_date_and_time = "2000-02-30 01:02:03" + @person.birth_date_and_time = "2000-01-32 02:03:04" @person.birth_date_and_time.should be_nil end it "should not save invalid date value to database" do - time_string = "2000-02-30 09:00:00" + time_string = "2000-01-32 02:03:04" @person = Person.new @person.birth_date_and_time = time_string @person.save @@ -41,7 +51,7 @@ describe ValidatesTimeliness::AttributeMethods do unless RAILS_VER < '2.1' it "should return stored time string as Time with correct timezone" do Time.zone = 'Melbourne' - time_string = "2000-06-01 01:02:03" + time_string = "2000-06-01 02:03:04" @person.birth_date_and_time = time_string @person.birth_date_and_time.strftime('%Y-%m-%d %H:%M:%S %Z %z').should == time_string + ' EST +1000' end @@ -57,17 +67,30 @@ describe ValidatesTimeliness::AttributeMethods do end it "should return true for attribute changed?" do - time_string = "2000-06-01 01:02:03" + time_string = "2000-06-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 for time attribute as nil to Time object" do - time_string = "2000-06-01 01:02:03" + time_string = "2000-06-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 + + else + + it "should return time object from database in default timezone" do + ActiveRecord::Base.default_timezone = :utc + time_string = "2000-06-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" do From 727f3dc8e36df1ec4414f087c64d84228610f2a3 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 22 Jul 2008 11:45:33 +1000 Subject: [PATCH 5/6] added make_time method to do time object creation with correct timezone handling for Rails 2.1 and 2.0 --- lib/validates_timeliness/validations.rb | 37 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/validates_timeliness/validations.rb b/lib/validates_timeliness/validations.rb index d003c46..536b9b1 100644 --- a/lib/validates_timeliness/validations.rb +++ b/lib/validates_timeliness/validations.rb @@ -21,7 +21,7 @@ module ValidatesTimeliness end module ClassMethods - + # Override this method to use any date parsing algorithm you like such as # Chronic. Just return nil for an invalid value and a Time object for a # valid parsed value. @@ -31,17 +31,14 @@ module ValidatesTimeliness time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict) raise if time_array.nil? - if type == :time - # Rails dummy time date part is defined as 2000-01-01 - time_array[0..2] = 2000, 1, 1 - end - + # Rails dummy time date part is defined as 2000-01-01 + time_array[0..2] = 2000, 1, 1 if type == :time + # Date.new enforces days per month, unlike Time - date = Date.new(*time_array[0..2]) unless type == :time - return date if type == :date + Date.new(*time_array[0..2]) unless type == :time - # Check time part, and return time object - Time.local(*time_array) rescue DateTime.new(*time_array[0..5]) + # Create time object which checks time part, and return time object + make_time(time_array) rescue nil end @@ -107,7 +104,7 @@ module ValidatesTimeliness private - # Validate value against the temoSpral restrictions. Restriction values + # Validate value against the temopral restrictions. Restriction values # maybe of mixed type, so the are evaluated as a common type, which may # require conversion. The type used is defined by validation type. def validate_timeliness_restrictions(record, attr_name, value, configuration) @@ -152,7 +149,23 @@ module ValidatesTimeliness defaults.each {|k, v| messages["#{k}_message".to_sym] = v } end end - + + # Create time in correct timezone. For Rails 2.1 that is value in + # Time.zone. Rails 2.0 should be default_timezone. + def make_time(time_array) + if Time.respond_to?(:zone) + Time.zone.local(*time_array) + else + begin + Time.send(ActiveRecord::Base.default_timezone, *time_array) + rescue ArgumentError, TypeError + zone_offset = ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0 + time_array.pop # remove microseconds + DateTime.civil(*(time_array << zone_offset)) + end + end + end + end end end From 28b44b4ca648aa2625058094f259ce4aa9dea968 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 22 Jul 2008 11:47:17 +1000 Subject: [PATCH 6/6] some docs tweaks and spec value changes --- lib/validates_timeliness/attribute_methods.rb | 7 +++--- spec/validations_spec.rb | 25 ++++++++----------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb index c535883..df859b6 100644 --- a/lib/validates_timeliness/attribute_methods.rb +++ b/lib/validates_timeliness/attribute_methods.rb @@ -16,8 +16,8 @@ module ValidatesTimeliness # for any subsequent differentiation. # # The wholesale replacement of the Rails time type casting is not done to - # preserve the quick conversion for timestamp columns and also any value which - # is never changed during the life of the record object. + # preserve the quickest conversion for timestamp columns and also any value + # which is never changed during the life of the record object. # # Dates are also handled but only write to cache value converted by plugin # parser. Default read method will retrieve from cache or do default @@ -35,7 +35,8 @@ module ValidatesTimeliness # Adds check for cached time attributes which have been type cast already # and value can be used from cache. This prevents the raw time value - # from being type cast using default Rails type casting. + # from being type cast using default Rails type casting when writing values + # to the database. def read_attribute(attr_name) attr_name = attr_name.to_s if !(value = @attributes[attr_name]).nil? diff --git a/spec/validations_spec.rb b/spec/validations_spec.rb index 55a78d8..d29d252 100644 --- a/spec/validations_spec.rb +++ b/spec/validations_spec.rb @@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/spec_helper' describe ValidatesTimeliness::Validations do before :all do # freezes time using time_travel plugin - Time.now = Time.utc(2008, 1, 1, 12, 0, 0) + Time.now = Time.utc(2000, 1, 1, 0, 0, 0) end after :all do @@ -34,11 +34,7 @@ describe ValidatesTimeliness::Validations do Time.zone.utc_offset.should == 10.hours end end - - it "should return Date object valid date string" do - parse_method("2000-02-01", :date).should be_kind_of(Date) - end - + it "should return nil for invalid date string" do parse_method("2000-02-30", :date).should be_nil end @@ -62,19 +58,19 @@ describe ValidatesTimeliness::Validations do end it "should have error for invalid date component for datetime column" do - @person.birth_date_and_time = "1980-02-30 01:02:03" + @person.birth_date_and_time = "2000-02-30 01:02:03" @person.should_not be_valid @person.errors.on(:birth_date_and_time).should == "is not a valid datetime" end it "should have error for invalid time component for datetime column" do - @person.birth_date_and_time = "1980-02-30 25:02:03" + @person.birth_date_and_time = "2000-02-30 25:02:03" @person.should_not be_valid @person.errors.on(:birth_date_and_time).should == "is not a valid datetime" end it "should have error for invalid date value for date column" do - @person.birth_date = "1980-02-30" + @person.birth_date = "2000-02-30" @person.should_not be_valid @person.errors.on(:birth_date).should == "is not a valid date" end @@ -86,8 +82,8 @@ describe ValidatesTimeliness::Validations do end it "should be valid with valid values" do - @person.birth_date_and_time = "1980-01-31 12:12:12" - @person.birth_date = "1980-01-31" + @person.birth_date_and_time = "2000-01-31 12:12:12" + @person.birth_date = "2000-01-31" @person.should be_valid end @@ -337,12 +333,13 @@ describe ValidatesTimeliness::Validations do describe "with mixed value and restriction types" do before :all do + class MixedBeforeAndAfter < Person validates_timeliness_of :birth_date_and_time, :before => Date.new(2008,1,2), - :after => lambda { Time.mktime(2008, 1, 1) } + :after => lambda { "2008-01-01" } validates_timeliness_of :birth_date, :type => :date, - :on_or_before => lambda { Time.mktime(2008, 1, 2) }, + :on_or_before => lambda { "2008-01-01" }, :on_or_after => :birth_date_and_time end end @@ -379,7 +376,7 @@ describe ValidatesTimeliness::Validations do end - describe "ignoring rstriction errors" do + describe "ignoring restriction errors" do before :all do class BadRestriction < Person validates_date :birth_date, :before => Proc.new { raise }