From 312c1510cb73dd05beef05a59b76715b2a282e58 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 28 Mar 2009 17:25:48 +1100 Subject: [PATCH 01/13] refactored AR parsing methods into Parser module to reduce AR method pollution and make more consistent --- lib/validates_timeliness.rb | 11 ++-- .../active_record/attribute_methods.rb | 7 +-- .../multiparameter_attributes.rb | 2 +- lib/validates_timeliness/parser.rb | 45 ++++++++++++++ .../validation_methods.rb | 36 ----------- lib/validates_timeliness/validator.rb | 10 +-- spec/active_record/attribute_methods_spec.rb | 6 +- spec/parser_spec.rb | 61 +++++++++++++++++++ spec/spec_helper.rb | 5 +- spec/validator_spec.rb | 8 ++- 10 files changed, 133 insertions(+), 58 deletions(-) create mode 100644 lib/validates_timeliness/parser.rb create mode 100644 spec/parser_spec.rb diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index 471db46..99e7715 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -1,4 +1,5 @@ require 'validates_timeliness/formats' +require 'validates_timeliness/parser' require 'validates_timeliness/validator' require 'validates_timeliness/validation_methods' require 'validates_timeliness/spec/rails/matchers/validate_timeliness' if ENV['RAILS_ENV'] == 'test' @@ -14,9 +15,11 @@ require 'validates_timeliness/core_ext/date_time' module ValidatesTimeliness mattr_accessor :default_timezone - self.default_timezone = :utc + mattr_accessor :use_time_zones + self.use_time_zones = false + LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/validates_timeliness/locale/en.yml') class << self @@ -46,10 +49,10 @@ module ValidatesTimeliness end def setup_for_rails - major, minor = Rails::VERSION::MAJOR, Rails::VERSION::MINOR self.default_timezone = ::ActiveRecord::Base.default_timezone - self.enable_datetime_select_extension! - self.load_error_messages + self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false + enable_datetime_select_extension! + load_error_messages end end end diff --git a/lib/validates_timeliness/active_record/attribute_methods.rb b/lib/validates_timeliness/active_record/attribute_methods.rb index 6ee1905..8630196 100644 --- a/lib/validates_timeliness/active_record/attribute_methods.rb +++ b/lib/validates_timeliness/active_record/attribute_methods.rb @@ -46,7 +46,7 @@ module ValidatesTimeliness # 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) - new = self.class.parse_date_time(value, type) + new = ValidatesTimeliness::Parser.parse(value, type) if new && type != :date new = new.to_time @@ -73,7 +73,7 @@ module ValidatesTimeliness if @attributes_cache.has_key?(attr_name) time = read_attribute_before_type_cast(attr_name) - time = self.class.parse_date_time(time, type) + time = ValidatesTimeliness::Parser.parse(time, type) else time = read_attribute(attr_name) @attributes[attr_name] = time && time_zone_aware ? time.in_time_zone : time @@ -83,8 +83,6 @@ module ValidatesTimeliness module ClassMethods - # Define attribute reader and writer method for date, time and - # datetime attributes to use plugin parser. def define_attribute_methods_with_timeliness return if generated_methods? columns_hash.each do |name, column| @@ -105,7 +103,6 @@ module ValidatesTimeliness define_attribute_methods_without_timeliness end - # Define write method for date, time and datetime columns def define_write_method_for_dates_and_times(attr_name, type, time_zone_aware) method_body = <<-EOV def #{attr_name}=(value) diff --git a/lib/validates_timeliness/active_record/multiparameter_attributes.rb b/lib/validates_timeliness/active_record/multiparameter_attributes.rb index a755e04..5727dfb 100644 --- a/lib/validates_timeliness/active_record/multiparameter_attributes.rb +++ b/lib/validates_timeliness/active_record/multiparameter_attributes.rb @@ -38,7 +38,7 @@ module ValidatesTimeliness end def time_array_to_string(values, type) - values = values.map {|v| v.to_s } + values.collect! {|v| v.to_s } case type when :date diff --git a/lib/validates_timeliness/parser.rb b/lib/validates_timeliness/parser.rb new file mode 100644 index 0000000..2f50f3e --- /dev/null +++ b/lib/validates_timeliness/parser.rb @@ -0,0 +1,45 @@ +module ValidatesTimeliness + module Parser + + class << self + + def parse(raw_value, type, strict=true) + return nil if raw_value.blank? + return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date) + + time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict) + raise if time_array.nil? + + # 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 + + # Create time object which checks time part, and return time object + make_time(time_array) + rescue + nil + end + + def make_time(time_array) + if Time.respond_to?(:zone) && ValidatesTimeliness.use_time_zones + Time.zone.local(*time_array) + else + begin + time_zone = ValidatesTimeliness.default_timezone + Time.send(time_zone, *time_array) + rescue ArgumentError, TypeError + zone_offset = time_zone == :local ? DateTime.local_offset : 0 + time_array.pop # remove microseconds + DateTime.civil(*(time_array << zone_offset)) + end + end + end + + end + + end +end diff --git a/lib/validates_timeliness/validation_methods.rb b/lib/validates_timeliness/validation_methods.rb index a6f7a76..5d23b09 100644 --- a/lib/validates_timeliness/validation_methods.rb +++ b/lib/validates_timeliness/validation_methods.rb @@ -7,27 +7,6 @@ module ValidatesTimeliness module ClassMethods - def parse_date_time(raw_value, type, strict=true) - return nil if raw_value.blank? - return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date) - - time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict) - raise if time_array.nil? - - # 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 - - # Create time object which checks time part, and return time object - make_time(time_array) - rescue - nil - end - def validates_time(*attr_names) configuration = attr_names.extract_options! configuration[:type] = :time @@ -59,21 +38,6 @@ module ValidatesTimeliness end end - # Time.zone. Rails 2.0 should be default_timezone. - def make_time(time_array) - if Time.respond_to?(:zone) && time_zone_aware_attributes - 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 diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 14ef01e..5b5d36a 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -36,7 +36,7 @@ module ValidatesTimeliness end def call(record, attr_name, value) - value = record.class.parse_date_time(value, type, false) if value.is_a?(String) + value = ValidatesTimeliness::Parser.parse(value, type, false) if value.is_a?(String) raw_value = raw_value(record, attr_name) || value return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank]) @@ -143,7 +143,7 @@ module ValidatesTimeliness end date, time = self.class.evaluate_option_value(date, :date, record), self.class.evaluate_option_value(time, :time, record) return if date.nil? || time.nil? - record.class.send(:make_time, [date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec]) + ValidatesTimeliness::Parser.make_time([date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec]) end def validate_options(options) @@ -158,7 +158,7 @@ module ValidatesTimeliness def evaluate_option_value(value, type, record) case value - when Time, Date, DateTime + when Time, Date value when Symbol evaluate_option_value(record.send(value), type, record) @@ -169,7 +169,7 @@ module ValidatesTimeliness when Range evaluate_option_value([value.first, value.last], type, record) else - record.class.parse_date_time(value, type, false) + ValidatesTimeliness::Parser.parse(value, type, false) end end @@ -192,7 +192,7 @@ module ValidatesTimeliness nil end if ignore_usec && value.is_a?(Time) - ::ActiveRecord::Base.send(:make_time, Array(value).reverse[4..9]) + ValidatesTimeliness::Parser.make_time(Array(value).reverse[4..9]) else value end diff --git a/spec/active_record/attribute_methods_spec.rb b/spec/active_record/attribute_methods_spec.rb index 5aba593..2cbad9c 100644 --- a/spec/active_record/attribute_methods_spec.rb +++ b/spec/active_record/attribute_methods_spec.rb @@ -39,17 +39,17 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do end it "should call parser on write for datetime attribute" do - @person.class.should_receive(:parse_date_time).once + ValidatesTimeliness::Parser.should_receive(:parse).once @person.birth_date_and_time = "2000-01-01 02:03:04" end it "should call parser on write for date attribute" do - @person.class.should_receive(:parse_date_time).once + ValidatesTimeliness::Parser.should_receive(:parse).once @person.birth_date = "2000-01-01" end it "should call parser on write for time attribute" do - @person.class.should_receive(:parse_date_time).once + ValidatesTimeliness::Parser.should_receive(:parse).once @person.birth_time = "12:00" end diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb new file mode 100644 index 0000000..b57d211 --- /dev/null +++ b/spec/parser_spec.rb @@ -0,0 +1,61 @@ +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') + +describe ValidatesTimeliness::Parser do + attr_accessor :person + + describe "parse" do + it "should return time object for valid time string" do + parse("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("2000-02-30 12:13:14", :datetime).should be_nil + end + + it "should return nil for time string with invalid time part" do + parse("2000-02-01 25:13:14", :datetime).should be_nil + end + + it "should return Time object when passed a Time object" do + parse(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("2000-01-01 12:13:14", :datetime) + Time.zone.utc_offset.should == 10.hours + end + end + + it "should return nil for invalid date string" do + parse("2000-02-30", :date).should be_nil + end + + def parse(*args) + ValidatesTimeliness::Parser.parse(*args) + end + end + + describe "make_time" do + + if RAILS_VER >= '2.1' + + it "should create time using current timezone" do + Time.zone = 'Melbourne' + time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0]) + time.zone.should == "EST" + end + + else + + it "should create time using default timezone" do + time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0]) + time.zone.should == "UTC" + end + + end + + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4425bea..4d50035 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -30,6 +30,7 @@ require 'active_record' require 'active_record/version' require 'action_controller' require 'action_view' +require 'action_mailer' require 'spec/rails' require 'time_travel/time_travel' @@ -38,13 +39,13 @@ ActiveRecord::Base.default_timezone = :utc RAILS_VER = Rails::VERSION::STRING puts "Using #{vendored ? 'vendored' : 'gem'} Rails version #{RAILS_VER} (ActiveRecord version #{ActiveRecord::VERSION::STRING})" -require 'validates_timeliness' - if RAILS_VER >= '2.1' Time.zone_default = ActiveSupport::TimeZone['UTC'] ActiveRecord::Base.time_zone_aware_attributes = true end +require 'validates_timeliness' + ActiveRecord::Migration.verbose = false ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'}) diff --git a/spec/validator_spec.rb b/spec/validator_spec.rb index 963c2f7..a1d9c78 100644 --- a/spec/validator_spec.rb +++ b/spec/validator_spec.rb @@ -66,7 +66,7 @@ describe ValidatesTimeliness::Validator do it "should return array of Time objects when restriction is array of strings" do time1, time2 = "2000-01-02", "2000-01-01" - evaluate_option_value([time1, time2], :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] + evaluate_option_value([time1, time2], :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)] end it "should return array of Time objects when restriction is Range of Time objects" do @@ -76,7 +76,7 @@ describe ValidatesTimeliness::Validator do it "should return array of Time objects when restriction is Range of time strings" do time1, time2 = "2000-01-02", "2000-01-01" - evaluate_option_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)] + evaluate_option_value(time1..time2, :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)] end def evaluate_option_value(restriction, type) configure_validator(:type => type) @@ -587,6 +587,10 @@ describe ValidatesTimeliness::Validator do end + def parse(*args) + ValidatesTimeliness::Parser.parse(*args) + end + def configure_validator(options={}) @validator = ValidatesTimeliness::Validator.new(options) end From c2a4f45b5afb3e9fec0c972665eace68af3f270f Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 28 Mar 2009 17:35:21 +1100 Subject: [PATCH 02/13] removed old spec --- spec/validation_methods_spec.rb | 61 --------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 spec/validation_methods_spec.rb diff --git a/spec/validation_methods_spec.rb b/spec/validation_methods_spec.rb deleted file mode 100644 index ab0f2cd..0000000 --- a/spec/validation_methods_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') - -describe ValidatesTimeliness::ValidationMethods do - attr_accessor :person - - describe "parse_date_time" 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 nil for invalid date string" do - parse_method("2000-02-30", :date).should be_nil - end - - def parse_method(*args) - ActiveRecord::Base.parse_date_time(*args) - end - end - - describe "make_time" do - - if RAILS_VER >= '2.1' - - it "should create time using current timezone" do - Time.zone = 'Melbourne' - time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0]) - time.zone.should == "EST" - end - - else - - it "should create time using default timezone" do - time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0]) - time.zone.should == "UTC" - end - - end - - end - -end From a836ed8434540bc7cc76e96a8732d877a7fc2996 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 28 Mar 2009 17:35:41 +1100 Subject: [PATCH 03/13] changed Formats#parse to take options hash for strict and other possibilities --- lib/validates_timeliness/formats.rb | 5 +++-- lib/validates_timeliness/parser.rb | 6 ++++-- lib/validates_timeliness/validator.rb | 4 ++-- spec/formats_spec.rb | 14 +++++++------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/validates_timeliness/formats.rb b/lib/validates_timeliness/formats.rb index 1e3f3ba..69a830f 100644 --- a/lib/validates_timeliness/formats.rb +++ b/lib/validates_timeliness/formats.rb @@ -161,12 +161,13 @@ module ValidatesTimeliness # pre or post match strings to exist if strict is false. Otherwise wrap # regexp in start and end anchors. # Returns 7 part time array. - def parse(string, type, strict=true) + def parse(string, type, options={}) return string unless string.is_a?(String) + options.reverse_merge!(:strict => true) matches = nil exp, processor = expression_set(type, string).find do |regexp, proc| - full = /\A#{regexp}\Z/ if strict + full = /\A#{regexp}\Z/ if options[:strict] full ||= case type when :date then /\A#{regexp}/ when :time then /#{regexp}\Z/ diff --git a/lib/validates_timeliness/parser.rb b/lib/validates_timeliness/parser.rb index 2f50f3e..90b47c5 100644 --- a/lib/validates_timeliness/parser.rb +++ b/lib/validates_timeliness/parser.rb @@ -3,11 +3,13 @@ module ValidatesTimeliness class << self - def parse(raw_value, type, strict=true) + def parse(raw_value, type, options={}) return nil if raw_value.blank? return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date) - time_array = ValidatesTimeliness::Formats.parse(raw_value, type, strict) + options.reverse_merge!(:strict => true) + + time_array = ValidatesTimeliness::Formats.parse(raw_value, type, options) raise if time_array.nil? # Rails dummy time date part is defined as 2000-01-01 diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 5b5d36a..2f2badd 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -36,7 +36,7 @@ module ValidatesTimeliness end def call(record, attr_name, value) - value = ValidatesTimeliness::Parser.parse(value, type, false) if value.is_a?(String) + value = ValidatesTimeliness::Parser.parse(value, type, :strict => false) if value.is_a?(String) raw_value = raw_value(record, attr_name) || value return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank]) @@ -169,7 +169,7 @@ module ValidatesTimeliness when Range evaluate_option_value([value.first, value.last], type, record) else - ValidatesTimeliness::Parser.parse(value, type, false) + ValidatesTimeliness::Parser.parse(value, type, :strict => false) end end diff --git a/spec/formats_spec.rb b/spec/formats_spec.rb index a166a76..3e71da6 100644 --- a/spec/formats_spec.rb +++ b/spec/formats_spec.rb @@ -139,37 +139,37 @@ describe ValidatesTimeliness::Formats do describe "extracting values" do it "should return time array from date string" do - time_array = formats.parse('12:13:14', :time, true) + time_array = formats.parse('12:13:14', :time, :strict => true) time_array.should == [0,0,0,12,13,14,0] end it "should return date array from time string" do - time_array = formats.parse('2000-02-01', :date, true) + time_array = formats.parse('2000-02-01', :date, :strict => true) time_array.should == [2000,2,1,0,0,0,0] end it "should return datetime array from string value" do - time_array = formats.parse('2000-02-01 12:13:14', :datetime, true) + time_array = formats.parse('2000-02-01 12:13:14', :datetime, :strict => true) time_array.should == [2000,2,1,12,13,14,0] end it "should parse date string when type is datetime" do - time_array = formats.parse('2000-02-01', :datetime, false) + time_array = formats.parse('2000-02-01', :datetime, :strict => false) time_array.should == [2000,2,1,0,0,0,0] end it "should ignore time when extracting date and strict is false" do - time_array = formats.parse('2000-02-01 12:12', :date, false) + time_array = formats.parse('2000-02-01 12:12', :date, :strict => false) time_array.should == [2000,2,1,0,0,0,0] end it "should ignore time when extracting date from format with trailing year and strict is false" do - time_array = formats.parse('01-02-2000 12:12', :date, false) + time_array = formats.parse('01-02-2000 12:12', :date, :strict => false) time_array.should == [2000,2,1,0,0,0,0] end it "should ignore date when extracting time and strict is false" do - time_array = formats.parse('2000-02-01 12:12', :time, false) + time_array = formats.parse('2000-02-01 12:12', :time, :strict => false) time_array.should == [0,0,0,12,12,0,0] end end From 7967b5a2122cef81c686941248fb5a3b98450563 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 28 Mar 2009 18:49:26 +1100 Subject: [PATCH 04/13] refactored error value formats to use locale file for I18n. Rail 2.0/2.1 to use default_error_value_formats now. moved default_error_messages_method into validator --- README.rdoc | 12 ++++++- lib/validates_timeliness.rb | 14 +++----- lib/validates_timeliness/locale/en.yml | 5 +++ .../rails/matchers/validate_timeliness.rb | 4 +-- lib/validates_timeliness/validator.rb | 35 ++++++++++++------- spec/validator_spec.rb | 16 +++++++-- 6 files changed, 58 insertions(+), 28 deletions(-) diff --git a/README.rdoc b/README.rdoc index 2848350..b9d97bf 100644 --- a/README.rdoc +++ b/README.rdoc @@ -302,12 +302,22 @@ will be inserted. And for something a little more specific you can override the format of the interpolation values inserted in the error messages for temporal restrictions like so - ValidatesTimeliness::Validator.error_value_formats.update( +For Rails 2.0/2.1: + + ValidatesTimeliness::Validator.default_error_value_formats.update( :time => '%H:%M:%S', :date => '%Y-%m-%d', :datetime => '%Y-%m-%d %H:%M:%S' ) +Rails 2.2+ using the I18n system to define new defaults: + + validates_timeliness: + error_value_formats: + date: '%Y-%m-%d' + time: '%H:%M:%S' + datetime: '%Y-%m-%d %H:%M:%S' + Those are Ruby strftime formats not the plugin formats. diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index 99e7715..06a66bc 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -34,20 +34,14 @@ module ValidatesTimeliness I18n.load_path += [ LOCALE_PATH ] I18n.reload! else - messages = YAML::load(IO.read(LOCALE_PATH)) - errors = messages['en']['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h } + defaults = YAML::load(IO.read(LOCALE_PATH))['en'] + errors = defaults['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h } ::ActiveRecord::Errors.default_error_messages.update(errors) + + ValidatesTimeliness::Validator.default_error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys end end - def default_error_messages - if Rails::VERSION::STRING < '2.2' - ::ActiveRecord::Errors.default_error_messages - else - I18n.translate('activerecord.errors.messages') - end - end - def setup_for_rails self.default_timezone = ::ActiveRecord::Base.default_timezone self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false diff --git a/lib/validates_timeliness/locale/en.yml b/lib/validates_timeliness/locale/en.yml index f8731c7..3b249a6 100644 --- a/lib/validates_timeliness/locale/en.yml +++ b/lib/validates_timeliness/locale/en.yml @@ -11,3 +11,8 @@ en: after: "must be after {{restriction}}" on_or_after: "must be on or after {{restriction}}" between: "must be between {{earliest}} and {{latest}}" + validates_timeliness: + error_value_formats: + date: '%Y-%m-%d' + time: '%H:%M:%S' + datetime: '%Y-%m-%d %H:%M:%S' diff --git a/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb b/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb index f215554..6587415 100644 --- a/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +++ b/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb @@ -116,7 +116,7 @@ module Spec end def error_message_for(option) - msg = @validator.send(:error_messages)[option] + msg = @validator.error_messages[option] restriction = @validator.class.send(:evaluate_option_value, @validator.configuration[option], @type, @record) if restriction @@ -135,7 +135,7 @@ module Spec def format_value(value) return value if value.is_a?(String) - value.strftime(ValidatesTimeliness::Validator.error_value_formats[@type]) + value.strftime(@validator.class.error_value_formats[@type]) end end diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 2f2badd..54ff4be 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -2,14 +2,9 @@ module ValidatesTimeliness class Validator cattr_accessor :ignore_restriction_errors - cattr_accessor :error_value_formats + cattr_accessor :default_error_value_formats self.ignore_restriction_errors = false - self.error_value_formats = { - :time => '%H:%M:%S', - :date => '%Y-%m-%d', - :datetime => '%Y-%m-%d %H:%M:%S' - } RESTRICTION_METHODS = { :equal_to => :==, @@ -47,7 +42,11 @@ module ValidatesTimeliness validate_restrictions(record, attr_name, value) end - + + def error_messages + @error_messages ||= self.class.default_error_messages.merge(custom_error_messages) + end + private def raw_value(record, attr_name) @@ -120,10 +119,6 @@ module ValidatesTimeliness end end - def error_messages - @error_messages ||= ValidatesTimeliness.default_error_messages.merge(custom_error_messages) - end - def custom_error_messages @custom_error_messages ||= configuration.inject({}) {|msgs, (k, v)| if md = /(.*)_message$/.match(k.to_s) @@ -132,7 +127,7 @@ module ValidatesTimeliness msgs } end - + def combine_date_and_time(value, record) if type == :date date = value @@ -156,6 +151,22 @@ module ValidatesTimeliness # class methods class << self + def default_error_messages + if Rails::VERSION::STRING < '2.2' + ::ActiveRecord::Errors.default_error_messages + else + I18n.translate('activerecord.errors.messages') + end + end + + def error_value_formats + if defined?(I18n) + I18n.translate('validates_timeliness.error_value_formats') + else + default_error_value_formats + end + end + def evaluate_option_value(value, type, record) case value when Time, Date diff --git a/spec/validator_spec.rb b/spec/validator_spec.rb index a1d9c78..36fa6f7 100644 --- a/spec/validator_spec.rb +++ b/spec/validator_spec.rb @@ -554,12 +554,18 @@ describe ValidatesTimeliness::Validator do describe "custom formats" do before :all do - @@formats = ValidatesTimeliness::Validator.error_value_formats - ValidatesTimeliness::Validator.error_value_formats = { + custom = { :time => '%H:%M %p', :date => '%d-%m-%Y', :datetime => '%d-%m-%Y %H:%M %p' } + + if defined?(I18n) + I18n.backend.store_translations 'en', :validates_timeliness => { :error_value_formats => custom } + else + @@formats = ValidatesTimeliness::Validator.default_error_value_formats + ValidatesTimeliness::Validator.default_error_value_formats = custom + end end it "should format datetime value of restriction" do @@ -581,7 +587,11 @@ describe ValidatesTimeliness::Validator do end after :all do - ValidatesTimeliness::Validator.error_value_formats = @@formats + if defined?(I18n) + I18n.reload! + else + ValidatesTimeliness::Validator.default_error_value_formats = @@formats + end end end From 956933f58be3ee0d9d461d7657e6041a87fb1597 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 28 Mar 2009 18:53:47 +1100 Subject: [PATCH 05/13] disable multiparameter values extension by default for v2 --- lib/validates_timeliness.rb | 1 - spec/spec_helper.rb | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index 06a66bc..b824c7f 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -45,7 +45,6 @@ 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 - enable_datetime_select_extension! load_error_messages end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d50035..55f542f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -46,6 +46,8 @@ end require 'validates_timeliness' +ValidatesTimeliness.enable_datetime_select_extension! + ActiveRecord::Migration.verbose = false ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'}) From f4ed751c26da1458be13fd7bc40dde27e4ed4146 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Sat, 28 Mar 2009 19:51:11 +1100 Subject: [PATCH 06/13] changed back to using error_value_formats for Rails 2.0/2.1 --- README.rdoc | 2 +- lib/validates_timeliness.rb | 2 +- lib/validates_timeliness/validator.rb | 8 +++++--- spec/validator_spec.rb | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.rdoc b/README.rdoc index b9d97bf..de7f8b3 100644 --- a/README.rdoc +++ b/README.rdoc @@ -304,7 +304,7 @@ values inserted in the error messages for temporal restrictions like so For Rails 2.0/2.1: - ValidatesTimeliness::Validator.default_error_value_formats.update( + ValidatesTimeliness::Validator.error_value_formats.update( :time => '%H:%M:%S', :date => '%Y-%m-%d', :datetime => '%Y-%m-%d %H:%M:%S' diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index b824c7f..fd6ef54 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -38,7 +38,7 @@ module ValidatesTimeliness errors = defaults['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h } ::ActiveRecord::Errors.default_error_messages.update(errors) - ValidatesTimeliness::Validator.default_error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys + ValidatesTimeliness::Validator.error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys end end diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 54ff4be..972a176 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -2,8 +2,6 @@ module ValidatesTimeliness class Validator cattr_accessor :ignore_restriction_errors - cattr_accessor :default_error_value_formats - self.ignore_restriction_errors = false RESTRICTION_METHODS = { @@ -163,10 +161,14 @@ module ValidatesTimeliness if defined?(I18n) I18n.translate('validates_timeliness.error_value_formats') else - default_error_value_formats + @@error_value_formats end end + def error_value_formats=(formats) + @@error_value_formats = formats + end + def evaluate_option_value(value, type, record) case value when Time, Date diff --git a/spec/validator_spec.rb b/spec/validator_spec.rb index 36fa6f7..5eff1d9 100644 --- a/spec/validator_spec.rb +++ b/spec/validator_spec.rb @@ -563,8 +563,8 @@ describe ValidatesTimeliness::Validator do if defined?(I18n) I18n.backend.store_translations 'en', :validates_timeliness => { :error_value_formats => custom } else - @@formats = ValidatesTimeliness::Validator.default_error_value_formats - ValidatesTimeliness::Validator.default_error_value_formats = custom + @@formats = ValidatesTimeliness::Validator.error_value_formats + ValidatesTimeliness::Validator.error_value_formats = custom end end @@ -590,7 +590,7 @@ describe ValidatesTimeliness::Validator do if defined?(I18n) I18n.reload! else - ValidatesTimeliness::Validator.default_error_value_formats = @@formats + ValidatesTimeliness::Validator.error_value_formats = @@formats end end end From 8303be05c3ccd82300a5a6e1a24c9eb708cc69a1 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Mon, 30 Mar 2009 21:03:51 +1100 Subject: [PATCH 07/13] little spec consistency --- spec/formats_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/formats_spec.rb b/spec/formats_spec.rb index 3e71da6..10eb58f 100644 --- a/spec/formats_spec.rb +++ b/spec/formats_spec.rb @@ -159,18 +159,18 @@ describe ValidatesTimeliness::Formats do end it "should ignore time when extracting date and strict is false" do - time_array = formats.parse('2000-02-01 12:12', :date, :strict => false) + time_array = formats.parse('2000-02-01 12:13', :date, :strict => false) time_array.should == [2000,2,1,0,0,0,0] end it "should ignore time when extracting date from format with trailing year and strict is false" do - time_array = formats.parse('01-02-2000 12:12', :date, :strict => false) + time_array = formats.parse('01-02-2000 12:13', :date, :strict => false) time_array.should == [2000,2,1,0,0,0,0] end it "should ignore date when extracting time and strict is false" do - time_array = formats.parse('2000-02-01 12:12', :time, :strict => false) - time_array.should == [0,0,0,12,12,0,0] + time_array = formats.parse('2000-02-01 12:13', :time, :strict => false) + time_array.should == [0,0,0,12,13,0,0] end end From fb520bbddc6cff7d9bce4df0ad1808b9a043fc32 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 31 Mar 2009 14:21:07 +1100 Subject: [PATCH 08/13] use plugin parser in action view extension --- lib/validates_timeliness/action_view/instance_tag.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validates_timeliness/action_view/instance_tag.rb b/lib/validates_timeliness/action_view/instance_tag.rb index 98529f8..48f4c5b 100644 --- a/lib/validates_timeliness/action_view/instance_tag.rb +++ b/lib/validates_timeliness/action_view/instance_tag.rb @@ -37,7 +37,7 @@ module ValidatesTimeliness return value_without_timeliness(object) end - time_array = ParseDate.parsedate(raw_value) + time_array = ValidatesTimeliness::Formats.parse(raw_value, :datetime) TimelinessDateTime.new(*time_array[0..5]) end From 9e2d95c3e1ca886238d397da22011743a7a5430f Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 31 Mar 2009 14:22:13 +1100 Subject: [PATCH 09/13] remove brittle and not very useful specs, which are covered elsewhere --- spec/formats_spec.rb | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/spec/formats_spec.rb b/spec/formats_spec.rb index 10eb58f..905d23f 100644 --- a/spec/formats_spec.rb +++ b/spec/formats_spec.rb @@ -6,46 +6,6 @@ describe ValidatesTimeliness::Formats do before do @formats = ValidatesTimeliness::Formats end - - describe "expression generator" do - it "should generate regexp for time" do - generate_regexp_str('hh:nn:ss').should == '/(\d{2}):(\d{2}):(\d{2})/' - end - - it "should generate regexp for time with meridian" do - generate_regexp_str('hh:nn:ss ampm').should == '/(\d{2}):(\d{2}):(\d{2}) ((?:[aApP])\.?[mM]\.?)/' - end - - it "should generate regexp for time with meridian and optional space between" do - generate_regexp_str('hh:nn:ss_ampm').should == '/(\d{2}):(\d{2}):(\d{2})\s?((?:[aApP])\.?[mM]\.?)/' - end - - it "should generate regexp for time with single or double digits" do - generate_regexp_str('h:n:s').should == '/(\d{1,2}):(\d{1,2}):(\d{1,2})/' - end - - it "should generate regexp for date" do - generate_regexp_str('yyyy-mm-dd').should == '/(\d{4})-(\d{2})-(\d{2})/' - end - - it "should generate regexp for date with slashes" do - generate_regexp_str('dd/mm/yyyy').should == '/(\d{2})\/(\d{2})\/(\d{4})/' - end - - it "should generate regexp for date with dots" do - generate_regexp_str('dd.mm.yyyy').should == '/(\d{2})\.(\d{2})\.(\d{4})/' - end - - it "should generate regexp for Ruby time string" do - expected = '/(\w{3,9}) (\w{3,9}) (\d{2}):(\d{2}):(\d{2}) (?:[+-]\d{2}:?\d{2}) (\d{4})/' - generate_regexp_str('ddd mmm hh:nn:ss zo yyyy').should == expected - end - - it "should generate regexp for iso8601 datetime" do - expected = '/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:Z|(?:[+-]\d{2}:?\d{2}))/' - generate_regexp_str('yyyy-mm-ddThh:nn:ss(?:Z|zo)').should == expected - end - end describe "format proc generator" do it "should generate proc which outputs date array with values in correct order" do From d89266d9f1e016939e8727858d91c60d5dc520ac Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 31 Mar 2009 21:25:05 +1100 Subject: [PATCH 10/13] minor stuff --- lib/validates_timeliness/validator.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/validates_timeliness/validator.rb b/lib/validates_timeliness/validator.rb index 972a176..06978c7 100644 --- a/lib/validates_timeliness/validator.rb +++ b/lib/validates_timeliness/validator.rb @@ -84,7 +84,7 @@ module ValidatesTimeliness restriction = [restriction] unless restriction.is_a?(Array) if defined?(I18n) - message = custom_error_messages[option] || I18n.translate('activerecord.errors.messages')[option] + message = custom_error_messages[option] || I18n.t('activerecord.errors.messages')[option] subs = message.scan(/\{\{([^\}]*)\}\}/) interpolations = {} subs.each_with_index {|s, i| interpolations[s[0].to_sym] = restriction[i].strftime(format) } @@ -150,16 +150,16 @@ module ValidatesTimeliness class << self def default_error_messages - if Rails::VERSION::STRING < '2.2' - ::ActiveRecord::Errors.default_error_messages + if defined?(I18n) + I18n.t('activerecord.errors.messages') else - I18n.translate('activerecord.errors.messages') + ::ActiveRecord::Errors.default_error_messages end end def error_value_formats if defined?(I18n) - I18n.translate('validates_timeliness.error_value_formats') + I18n.t('validates_timeliness.error_value_formats') else @@error_value_formats end From 1e3c80203102d33f8db1cd2acb9b7f54c5606836 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Tue, 31 Mar 2009 21:35:49 +1100 Subject: [PATCH 11/13] capture zone offset value in formats to possible usage --- lib/validates_timeliness/formats.rb | 27 +++++++++++++++------------ spec/formats_spec.rb | 9 +++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/validates_timeliness/formats.rb b/lib/validates_timeliness/formats.rb index 69a830f..7082752 100644 --- a/lib/validates_timeliness/formats.rb +++ b/lib/validates_timeliness/formats.rb @@ -124,13 +124,13 @@ module ValidatesTimeliness { 's' => [ /s{1}/, '(\d{1,2})', :sec ] }, { 'u' => [ /u{1,}/, '(\d{1,6})', :usec ] }, { 'ampm' => [ /ampm/, '((?:[aApP])\.?[mM]\.?)', :meridian ] }, - { 'zo' => [ /zo/, '(?:[+-]\d{2}:?\d{2})'] }, + { 'zo' => [ /zo/, '([+-]\d{2}:?\d{2})', :offset ] }, { 'tz' => [ /tz/, '(?:[A-Z]{1,4})' ] }, { '_' => [ /_/, '\s?' ] } ] - # Arguments whichs will be passed to the format proc if matched in the - # time string. The key must should the key from the format tokens. The array + # Arguments which will be passed to the format proc if matched in the + # time string. The key must be the key from the format tokens. The array # consists of the arry position of the arg, the arg name, and the code to # place in the time array slot. The position can be nil which means the arg # won't be placed in the array. @@ -146,6 +146,7 @@ module ValidatesTimeliness :min => [4, 'n', 'n'], :sec => [5, 's', 's'], :usec => [6, 'u', 'microseconds(u)'], + :offset => [7, 'z', 'offset_in_seconds(z)'], :meridian => [nil, 'md', nil] } @@ -175,7 +176,8 @@ module ValidatesTimeliness end matches = full.match(string.strip) end - processor.call(*matches[1..7]) if matches + last = options[:include_offset] ? 8 : 7 + processor.call(*matches[1..last]) if matches end # Delete formats of specified type. Error raised if format not found. @@ -207,8 +209,7 @@ module ValidatesTimeliness end compile_format_expressions end - - + # Removes formats where the 1 or 2 digit month comes first, to eliminate # formats which are ambiguous with the European style of day then month. # The mmm token is ignored as its not ambigous. @@ -247,17 +248,12 @@ module ValidatesTimeliness # argument in the position indicated by the first element of the proc arg # array. # - # Examples: - # - # 'yyyy-mm-dd hh:nn' => lambda {|y,m,d,h,n| md||=0; [unambiguous_year(y),month_index(m),d,full_hour(h,md),n,nil,nil].map {|i| i.to_i } } - # 'dd/mm/yyyy h:nn_ampm' => lambda {|d,m,y,h,n,md| md||=0; [unambiguous_year(y),month_index(m),d,full_hour(h,md),n,nil,nil].map {|i| i.to_i } } - # def format_proc(order) arg_map = format_proc_args args = order.invert.sort.map {|p| arg_map[p[1]][1] } arr = [nil] * 7 order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? } - proc_string = "lambda {|#{args.join(',')}| md||=nil; [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.to_i } }" + proc_string = "lambda {|#{args.join(',')}| md||=nil; [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.is_a?(Float) ? i : i.to_i } }" eval proc_string end @@ -314,6 +310,13 @@ module ValidatesTimeliness def microseconds(usec) (".#{usec}".to_f * 1_000_000).to_i end + + def offset_in_seconds(offset) + sign = offset =~ /^-/ ? -1 : 1 + parts = offset.scan(/\d\d/).map {|p| p.to_f } + parts[1] = parts[1].to_f / 60 + (parts[0] + parts[1]) * sign * 3600 + end end end end diff --git a/spec/formats_spec.rb b/spec/formats_spec.rb index 905d23f..2c233c9 100644 --- a/spec/formats_spec.rb +++ b/spec/formats_spec.rb @@ -31,6 +31,10 @@ describe ValidatesTimeliness::Formats do it "should generate proc which outputs time array with microseconds" do generate_proc('hh:nn:ss.u').call('01', '02', '03', '99').should == [0,0,0,1,2,3,990000] end + + it "should generate proc which outputs datetime array with zone offset" do + generate_proc('yyyy-mm-dd hh:nn:ss.u zo').call('2001', '02', '03', '04', '05', '06', '99', '+10:00').should == [2001,2,3,4,5,6,990000,36000] + end end describe "validation regexps" do @@ -132,6 +136,11 @@ describe ValidatesTimeliness::Formats do time_array = formats.parse('2000-02-01 12:13', :time, :strict => false) time_array.should == [0,0,0,12,13,0,0] end + + it "should return zone offset when :include_offset options is true" do + time_array = formats.parse('2000-02-01T12:13:14-10:30', :datetime, :include_offset => true) + time_array.should == [2000,2,1,12,13,14,0,-37800] + end end describe "removing formats" do From 6d12790d2bc2bf39bce1e6f78280ce0c2af56287 Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Mon, 6 Apr 2009 17:34:23 +1000 Subject: [PATCH 12/13] datetime select helper extension activation in readme --- README.rdoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rdoc b/README.rdoc index de7f8b3..62a3255 100644 --- a/README.rdoc +++ b/README.rdoc @@ -266,6 +266,20 @@ corner cases a little harder to test. In general if you are using procs or model methods and you only care when they return a value, then they should return nil in all other situations. Restrictions are skipped if they are nil. + +=== DISPLAY INVALID VALUES IN DATE HELPERS: + +The plugin has some extensions to ActionView and ActiveRecord by allowing invalid +date and time values to be redisplayed to the user as feedback, instead of +a blank field which happens by default in Rails. Though the date helpers make this a +pretty rare occurence, given the select dropdowns for each date/time component, but +it may be something of interest. + +To activate it, put this in an initializer: + + ValidatesTimeliness.enable_datetime_select_extension! + + === OTHER CUSTOMISATION: The error messages for each temporal restrictions can also be globally overridden by From 613001791a9c3da4cbb50710d6942665f0cd9a0a Mon Sep 17 00:00:00 2001 From: Adam Meehan Date: Thu, 9 Apr 2009 18:53:28 +1000 Subject: [PATCH 13/13] limit time_array size explicity --- lib/validates_timeliness/parser.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/validates_timeliness/parser.rb b/lib/validates_timeliness/parser.rb index 90b47c5..7b7845c 100644 --- a/lib/validates_timeliness/parser.rb +++ b/lib/validates_timeliness/parser.rb @@ -20,8 +20,7 @@ module ValidatesTimeliness return date if type == :date - # Create time object which checks time part, and return time object - make_time(time_array) + make_time(time_array[0..7]) rescue nil end