Compare commits

..

10 Commits
1.1.1 ... 1.1.3

11 changed files with 149 additions and 92 deletions

View File

@@ -1,3 +1,11 @@
= 1.1.3 [2009-01-13]
- Fixed bug where time and date attributes still being parsed on read using Rails default parser [reported by Brad (pvjq)]
= 1.1.2 [2009-01-12]
- Fixed bugs
- matcher failing for custom error message without interpolation keys using I18n
- validator custom error messages not being extracted
= 1.1.1 [2009-01-03]
- Fixed bug in matcher for options local variable

View File

@@ -5,7 +5,7 @@ require 'date'
require 'spec/rake/spectask'
GEM = "validates_timeliness"
GEM_VERSION = "1.1.1"
GEM_VERSION = "1.1.3"
AUTHOR = "Adam Meehan"
EMAIL = "adam.meehan@gmail.com"
HOMEPAGE = "http://github.com/adzap/validates_timeliness"

6
TODO
View File

@@ -1,7 +1,5 @@
- :format option
- :with_date and :with_time options
- Merb and Data Mapper support
- does it have before_type_cast
- timezone handling
- view helper support
- valid formats could come from locale file
- formats to use month and day names from i18n
- add replace_formats instead add_formats :before

View File

@@ -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

View File

@@ -10,16 +10,14 @@ module ValidatesTimeliness
# string values.
#
class Formats
cattr_accessor :time_formats
cattr_accessor :date_formats
cattr_accessor :datetime_formats
cattr_accessor :time_expressions
cattr_accessor :date_expressions
cattr_accessor :datetime_expressions
cattr_accessor :format_tokens
cattr_accessor :format_proc_args
cattr_accessor :time_formats,
:date_formats,
:datetime_formats,
:time_expressions,
:date_expressions,
:datetime_expressions,
:format_tokens,
:format_proc_args
# Format tokens:
# y = year
@@ -164,16 +162,17 @@ module ValidatesTimeliness
def parse(string, type, strict=true)
return string unless string.is_a?(String)
expressions = expression_set(type, string)
time_array = nil
expressions.each do |(regexp, processor)|
regexp = strict || type == :datetime ? /\A#{regexp}\Z/ : (type == :date ? /\A#{regexp}/ : /#{regexp}\Z/)
if matches = regexp.match(string.strip)
time_array = processor.call(*matches[1..7])
break
matches = nil
exp, processor = expression_set(type, string).find do |regexp, proc|
full = /\A#{regexp}\Z/ if strict
full ||= case type
when :datetime then /\A#{regexp}\Z/
when :date then /\A#{regexp}/
else /#{regexp}\Z/
end
matches = full.match(string.strip)
end
return time_array
processor.call(*matches[1..7]) if matches
end
# Delete formats of specified type. Error raised if format not found.
@@ -223,7 +222,7 @@ module ValidatesTimeliness
def format_expression_generator(string_format)
regexp = string_format.dup
order = {}
regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes ]/
regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
format_tokens.each do |token|
token_name = token.keys.first
@@ -260,7 +259,7 @@ module ValidatesTimeliness
end
def compile_formats(formats)
formats.collect { |format| regexp, format_proc = format_expression_generator(format) }
formats.map { |format| regexp, format_proc = format_expression_generator(format) }
end
# Pick expression set and combine date and datetimes for

View File

@@ -123,7 +123,8 @@ module Spec
restriction = [restriction] unless restriction.is_a?(Array)
restriction.map! {|r| @validator.send(:type_cast_value, r) }
interpolate = @validator.send(:interpolation_values, option, restriction )
if defined?(I18n)
# get I18n message if defined and has interpolation keys in msg
if defined?(I18n) && !@validator.send(:custom_error_messages).include?(option)
msg = @record.errors.generate_message(@expected, option, interpolate)
else
msg = msg % interpolate

View File

@@ -115,7 +115,7 @@ module ValidatesTimeliness
return @custom_error_messages if defined?(@custom_error_messages)
@custom_error_messages = configuration.inject({}) {|msgs, (k, v)|
if md = /(.*)_message$/.match(k.to_s)
msgs[md[0].to_sym] = v
msgs[md[1].to_sym] = v
end
msgs
}

View File

@@ -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"

View File

@@ -5,27 +5,39 @@ end
class WithValidation < Person
validates_date :birth_date,
:before => '2000-01-10', :after => '2000-01-01',
:on_or_before => '2000-01-09', :on_or_after => '2000-01-02',
:before => '2000-01-10',
:after => '2000-01-01',
:on_or_before => '2000-01-09',
:on_or_after => '2000-01-02',
:between => ['2000-01-01', '2000-01-03']
validates_time :birth_time,
:before => '23:00', :after => '09:00',
:on_or_before => '22:00', :on_or_after => '10:00',
:before => '23:00',
:after => '09:00',
:on_or_before => '22:00',
:on_or_after => '10:00',
:between => ['09:00', '17:00']
validates_datetime :birth_date_and_time,
:before => '2000-01-10 23:00', :after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00', :on_or_after => '2000-01-02 09:00',
:before => '2000-01-10 23:00',
:after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00',
:on_or_after => '2000-01-02 09:00',
:between => ['2000-01-01 09:00', '2000-01-01 17:00']
end
class CustomMessages < Person
validates_date :birth_date, :invalid_date_message => 'is not really a date',
:before => '2000-01-10', :before_message => 'is too late',
:after => '2000-01-01', :after_message => 'is too early',
:on_or_before=> '2000-01-09', :on_or_before_message => 'is just too late',
:on_or_after => '2000-01-02', :on_or_after_message => 'is just too early'
validates_date :birth_date,
:invalid_date_message => 'is not really a date',
:before => '2000-01-10',
:before_message => 'is too late',
:after => '2000-01-01',
:after_message => 'is too early',
:on_or_before => '2000-01-09',
:on_or_before_message => 'is just too late',
:on_or_after => '2000-01-02',
:on_or_after_message => 'is just too early'
end
describe "ValidateTimeliness matcher" do

View File

@@ -363,6 +363,40 @@ describe ValidatesTimeliness::Validator do
end
end
describe "custom_error_messages" do
it "should return hash of custom error messages from configuration with _message truncated from keys" do
configure_validator(:type => :date, :invalid_date_message => 'thats no date')
validator.send(:custom_error_messages)[:invalid_date].should == 'thats no date'
end
it "should return empty hash if no custom error messages in configuration" do
configure_validator(:type => :date)
validator.send(:custom_error_messages).should be_empty
end
end
describe "interpolation_values" do
if defined?(I18n)
it "should return hash of interpolation keys with restriction values" do
before = '1900-01-01'
configure_validator(:type => :date, :before => before)
validator.send(:interpolation_values, :before, before.to_date).should == {:restriction => before}
end
it "should return empty hash if no interpolation keys are in message" do
before = '1900-01-01'
configure_validator(:type => :date, :before => before, :before_message => 'too late')
validator.send(:interpolation_values, :before, before.to_date).should be_empty
end
else
it "should return array of interpolation values" do
before = '1900-01-01'
configure_validator(:type => :date, :before => before)
validator.send(:interpolation_values, :before, before.to_date).should == [before]
end
end
end
describe "restriction errors" do
before :each do
configure_validator(:type => :date, :before => lambda { raise })

View File

@@ -2,12 +2,12 @@
Gem::Specification.new do |s|
s.name = %q{validates_timeliness}
s.version = "1.1.1"
s.version = "1.1.3"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Adam Meehan"]
s.autorequire = %q{validates_timeliness}
s.date = %q{2009-01-03}
s.date = %q{2009-01-13}
s.description = %q{Date and time validation plugin for Rails 2.x which allows custom formats}
s.email = %q{adam.meehan@gmail.com}
s.extra_rdoc_files = ["README.rdoc", "LICENSE", "TODO", "CHANGELOG"]