Compare commits

..

36 Commits
1.0.0 ... 1.1.5

Author SHA1 Message Date
Adam Meehan
b3e235a8a1 release 1.1.5 2009-01-21 14:15:35 +11:00
Adam Meehan
71583805c8 fixed regex for yy format token which wasn't greedy enough when datetime string parsed as date causing a 4 digit year to be extracted as first 2 digits 2009-01-21 14:07:35 +11:00
Adam Meehan
2ee971623c whitespace 2009-01-21 14:07:20 +11:00
Adam Meehan
817e49940c removed the 'resume' call, um wtf? 2009-01-21 14:05:46 +11:00
Adam Meehan
a76fc112e7 release 1.1.4 2009-01-13 20:11:04 +11:00
Adam Meehan
575ff85346 Merge branch 'months' 2009-01-13 20:07:54 +11:00
Adam Meehan
7ed76b5161 removed i18n of month names from TODO 2009-01-13 20:07:24 +11:00
Adam Meehan
65ed8a657e format months names now respect i18n 2009-01-13 20:05:55 +11:00
Adam Meehan
360108c39f release version 1.1.3 2009-01-13 10:25:27 +11:00
Adam Meehan
0ad8ace335 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) 2009-01-13 10:12:41 +11:00
Adam Meehan
7c9ec695f4 small refactor and cleanup of formats class 2009-01-12 21:42:14 +11:00
Adam Meehan
6af61917dd release v1.1.2 2009-01-12 13:20:58 +11:00
Adam Meehan
43e6748cd2 actually removed DM and Merb todos this time and added month name i18n handling and remove_formats 2009-01-12 13:12:51 +11:00
Adam Meehan
760a52a2a4 cleanup matcher spec a little 2009-01-12 13:08:22 +11:00
Adam Meehan
9f1642c730 fix matcher error_message_for when i18n is loaded and custom error message to use regular string interpolation 2009-01-12 13:04:41 +11:00
Adam Meehan
011ea070db fix interpolation_values examples for rails version without i18n 2009-01-12 13:03:21 +11:00
Adam Meehan
b632093ce2 add examples for custom_error_messages and interpolation values. what can I say? TATFT 2009-01-12 12:36:37 +11:00
Adam Meehan
525b3b9941 fix custom_error_message hash bug using wrong match data index 2009-01-12 12:35:35 +11:00
Adam Meehan
db8dd9ac99 version 1.1.1 2009-01-03 19:11:36 +11:00
Adam Meehan
1fdfc23cb8 fixed bug in matcher using local variable for options, must not have run the specs one last time before last release. Umm marr 2009-01-03 19:07:27 +11:00
Adam Meehan
7d3ee4bc1b added rspec matcher to features list 2009-01-02 12:15:09 +11:00
Adam Meehan
694a4bdd69 change error to warning for untested Rails version 2009-01-01 20:53:19 +11:00
Adam Meehan
07359c6157 updated TODO to remove between and DM and Merb support in light of Rails 3 merge 2009-01-01 20:51:49 +11:00
Adam Meehan
215b3dedfd updated changelog for v1.1 2009-01-01 20:38:09 +11:00
Adam Meehan
753a63417b version bumped to 1.1 2009-01-01 20:36:57 +11:00
Adam Meehan
af923014f5 added ignore file 2009-01-01 20:30:32 +11:00
Adam Meehan
a7c6e37333 Merge branch 'between' 2009-01-01 20:30:22 +11:00
Adam Meehan
a14bc306b3 added between option details to README 2009-01-01 20:28:02 +11:00
Adam Meehan
a71d6f7945 added between option testing to matcher and refactored 2009-01-01 20:13:44 +11:00
Adam Meehan
45ab815039 added between option and some refactoring 2009-01-01 20:11:30 +11:00
Adam Meehan
c308aaf4a9 refactored attribute name handling in spec 2008-12-28 17:22:24 +11:00
Adam Meehan
6584d0f1f0 removed random blob of code in readme 2008-12-10 08:41:31 +11:00
Adam Meehan
5abaec66ae remove version check, check for I what I want 2008-12-09 16:56:03 +11:00
Adam Meehan
ea5452a604 installation instructions for plugin corrected 2008-12-08 08:38:04 +11:00
Adam Meehan
40437c970d make format error raise with message 2008-12-07 21:29:50 +11:00
Adam Meehan
37bfbfe5e7 doc update with new feature 2008-12-07 18:01:01 +11:00
16 changed files with 475 additions and 239 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
pkg/

View File

@@ -1,3 +1,23 @@
= 1.1.5 [2009-01-21]
- Fixed regex for 'yy' format token which wasn't greedy enough for date formats ending with year when a datetime string parsed as date with a 4 digit year
= 1.1.4 [2009-01-13]
- Make months names respect i18n in Formats
= 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
= 1.1.0 [2009-01-01]
- Added between option
= 1.0.0 [2008-12-06]
- Gemified!
- Refactor of plugin into a Data Mapper style validator class which makes for a cleaner implementation and possible future Merb\Data Mapper support

View File

@@ -1,7 +1,7 @@
= validates_timeliness
* Source: http://github.com/adzap/validates_timeliness
* Bugs: http://adzap.lighthouseapp.com/projects/14111-validates_timeliness
* Source: http://github.com/adzap/validates_timeliness
* Bugs: http://adzap.lighthouseapp.com/projects/14111-validates_timeliness
== DESCRIPTION:
@@ -24,12 +24,16 @@ think should be a valid date or time string.
* Respects new timezone features of Rails 2.1.
* Supports Rails 2.2 I18n for the error messages
* Rspec matcher for testing model validation of dates and times
== INSTALLATION:
As plugin (from master)
./script/plugin git://github.com/adzap/validates_timeliness
./script/plugin git://github.com/adzap/validates_timeliness.git
As gem
@@ -62,6 +66,7 @@ the valid range of dates or times allowed
:on_or_before - Attribute must be equal to or before this value to be valid
:after - Attribute must be after this value to be valid
:on_or_after - Attribute must be equal to or after this value to be valid
:between - Attribute must be between the values to be valid
Regular validation options:
:allow_nil - Allow a nil value to be valid
@@ -77,6 +82,7 @@ the valid range of dates or times allowed
:on_or_before_message
:after_message
:on_or_after_message
:between_message
The temporal restrictions can take 4 different value types:
@@ -84,6 +90,7 @@ The temporal restrictions can take 4 different value types:
* Date, Time, or DateTime object value
* Proc or lambda object
* A symbol matching the method name in the model
* Between option takes an array of two values or a range
When an attribute value is compared to temporal restrictions, they are compared as
the same type as the validation method type. So using validates_date means all
@@ -193,7 +200,7 @@ of d/my/yy. By default the plugin uses the US formats as this is the Ruby defaul
when it does date interpretation, and is in keeping PoLS (principle of least
surprise).
To switch to using the :after => 1.day.from_nowEuropean (or Rest of The World) formats put this in an
To switch to using the European (or Rest of The World) formats put this in an
initializer or environment.rb
ValidatesTimeliness::Formats.remove_us_formats
@@ -264,7 +271,8 @@ For Rails 2.0/2.1:
:before => "must be before %s",
:on_or_before => "must be on or before %s",
:after => "must be after %s",
:on_or_after => "must be on or after %s"
:on_or_after => "must be on or after %s",
:between => "must be between %s and %s"
)
Where %s is the interpolation value for the restriction.
@@ -275,8 +283,9 @@ Rails 2.2+ using the I18n system to define new defaults:
activerecord:
errors:
messages:
on_or_before: "must equal to or before {{restriction}}"
on_or_after: "must equal to or after {{restriction}}"
on_or_before: "must be equal to or before {{restriction}}"
on_or_after: "must be equal to or after {{restriction}}"
between: "must be between {{earliest}} and {{latest}}"
The {{restriction}} signifies where the interpolation value for the restriction
will be inserted.

View File

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

6
TODO
View File

@@ -1,8 +1,4 @@
- :between option
- :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
- add replace_formats instead add_formats :before

View File

@@ -53,10 +53,10 @@ module ValidatesTimeliness
def setup_for_rails
major, minor = Rails::VERSION::MAJOR, Rails::VERSION::MINOR
self.send("setup_for_rails_#{major}_#{minor}")
self.default_timezone = ::ActiveRecord::Base.default_timezone
self.send("setup_for_rails_#{major}_#{minor}")
rescue
raise "Rails version #{Rails::VERSION::STRING} not yet supported by validates_timeliness plugin"
puts "Rails version #{major}.#{minor}.x not explicitly supported by validates_timeliness plugin. You may encounter some problems."
end
end
end

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

@@ -1,3 +1,5 @@
require 'date'
module ValidatesTimeliness
# A date and time format regular expression generator. Allows you to
@@ -10,16 +12,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
@@ -115,7 +115,7 @@ module ValidatesTimeliness
{ 'mm' => [ /m{2}/, '(\d{2})', :month ] },
{ 'm' => [ /(\A|[^ap])m{1}/, '(\d{1,2})', :month ] },
{ 'yyyy' => [ /y{4,}/, '(\d{4})', :year ] },
{ 'yy' => [ /y{2,}/, '(\d{2}|\d{4})', :year ] },
{ 'yy' => [ /y{2,}/, '(\d{4}|\d{2})', :year ] },
{ 'hh' => [ /h{2,}/, '(\d{2})', :hour ] },
{ 'h' => [ /h{1}/, '(\d{1,2})', :hour ] },
{ 'nn' => [ /n{2,}/, '(\d{2})', :min ] },
@@ -164,16 +164,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 +224,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
@@ -237,8 +238,7 @@ module ValidatesTimeliness
return Regexp.new(regexp), format_proc(order)
rescue
puts "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}."
raise
raise "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}."
end
# Generates a proc which when executed maps the regexp capture groups to a
@@ -261,7 +261,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
@@ -299,7 +299,15 @@ module ValidatesTimeliness
def month_index(month)
return month.to_i if month.to_i.nonzero?
Date::ABBR_MONTHNAMES.index(month.capitalize) || Date::MONTHNAMES.index(month.capitalize)
abbr_month_names.index(month.capitalize) || month_names.index(month.capitalize)
end
def month_names
defined?(I18n) ? I18n.t('date.month_names') : Date::MONTHNAMES
end
def abbr_month_names
defined?(I18n) ? I18n.t('date.abbr_month_names') : Date::ABBR_MONTHNAMES
end
def microseconds(usec)

View File

@@ -9,3 +9,4 @@ en:
on_or_before: "must be on or before {{restriction}}"
after: "must be after {{restriction}}"
on_or_after: "must be on or after {{restriction}}"
between: "must be between {{earliest}} and {{latest}}"

View File

@@ -2,120 +2,157 @@ module Spec
module Rails
module Matchers
class ValidateTimeliness
cattr_accessor :test_values
@@test_values = {
VALIDITY_TEST_VALUES = {
:date => {:pass => '2000-01-01', :fail => '2000-01-32'},
:time => {:pass => '12:00', :fail => '25:00'},
:datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'}
}
OPTION_TEST_SETTINGS = {
:before => { :method => :-, :modify_on => :valid },
:after => { :method => :+, :modify_on => :valid },
:on_or_before => { :method => :+, :modify_on => :invalid },
:on_or_after => { :method => :-, :modify_on => :invalid }
}
def initialize(attribute, options)
@expected, @options = attribute, options
@validator = ValidatesTimeliness::Validator.new(options)
compile_error_messages
end
def compile_error_messages
messages = validator.send(:error_messages)
@messages = messages.inject({}) {|h, (k, v)| h[k] = v.gsub(/ (\%s|\{\{\w*\}\})/, ''); h }
end
def matches?(record)
@record = record
type = options[:type]
@type = @options[:type]
invalid_value = @@test_values[type][:fail]
valid_value = parse_and_cast(@@test_values[type][:pass])
valid = error_matching(invalid_value, /#{messages["invalid_#{type}".to_sym]}/) &&
no_error_matching(valid_value, /#{messages["invalid_#{type}".to_sym]}/)
valid = test_validity
valid = test_option(:before, :-) if options[:before] && valid
valid = test_option(:after, :+) if options[:after] && valid
valid = test_option(:before) if @options[:before] && valid
valid = test_option(:after) if @options[:after] && valid
valid = test_option(:on_or_before, :+, :modify_on => :invalid) if options[:on_or_before] && valid
valid = test_option(:on_or_after, :-, :modify_on => :invalid) if options[:on_or_after] && valid
valid = test_option(:on_or_before) if @options[:on_or_before] && valid
valid = test_option(:on_or_after) if @options[:on_or_after] && valid
valid = test_between if @options[:between] && valid
return valid
end
def failure_message
"expected model to validate #{options[:type]} attribute #{expected.inspect} with #{last_failure}"
"expected model to validate #{@type} attribute #{@expected.inspect} with #{@last_failure}"
end
def negative_failure_message
"expected not to validate #{options[:type]} attribute #{expected.inspect}"
"expected not to validate #{@type} attribute #{@expected.inspect}"
end
def description
"have validated #{options[:type]} attribute #{expected.inspect}"
"have validated #{@type} attribute #{@expected.inspect}"
end
private
attr_reader :actual, :expected, :record, :options, :messages, :last_failure, :validator
def test_option(option, modifier, settings={})
settings.reverse_merge!(:modify_on => :valid)
boundary = parse_and_cast(options[option])
valid_value, invalid_value = if settings[:modify_on] == :valid
[ boundary.send(modifier, 1), boundary ]
else
[ boundary, boundary.send(modifier, 1) ]
def test_validity
invalid_value = VALIDITY_TEST_VALUES[@type][:fail]
valid_value = parse_and_cast(VALIDITY_TEST_VALUES[@type][:pass])
error_matching(invalid_value, "invalid_#{@type}".to_sym) &&
no_error_matching(valid_value, "invalid_#{@type}".to_sym)
end
message = messages[option]
error_matching(invalid_value, /#{message}/) &&
no_error_matching(valid_value, /#{message}/)
def test_option(option)
settings = OPTION_TEST_SETTINGS[option]
boundary = parse_and_cast(@options[option])
method = settings[:method]
valid_value, invalid_value = if settings[:modify_on] == :valid
[ boundary.send(method, 1), boundary ]
else
[ boundary, boundary.send(method, 1) ]
end
error_matching(invalid_value, option) &&
no_error_matching(valid_value, option)
end
def test_before
before = parse_and_cast(@options[:before])
error_matching(before - 1, :before) &&
no_error_matching(before, :before)
end
def test_between
between = parse_and_cast(@options[:between])
error_matching(between.first - 1, :between) &&
error_matching(between.last + 1, :between) &&
no_error_matching(between.first, :between) &&
no_error_matching(between.last, :between)
end
def parse_and_cast(value)
value = validator.send(:restriction_value, value, record)
validator.send(:type_cast_value, value)
value = @validator.send(:restriction_value, value, @record)
@validator.send(:type_cast_value, value)
end
def error_matching(value, match)
record.send("#{expected}=", value)
record.valid?
errors = record.errors.on(expected)
pass = [ errors ].flatten.any? {|error| match === error }
@last_failure = "error matching #{match.inspect} when value is #{format_value(value)}" unless pass
def error_matching(value, option)
match = error_message_for(option)
@record.send("#{@expected}=", value)
@record.valid?
errors = @record.errors.on(@expected)
pass = [ errors ].flatten.any? {|error| /#{match}/ === error }
@last_failure = "error matching '#{match}' when value is #{format_value(value)}" unless pass
pass
end
def no_error_matching(value, match)
pass = !error_matching(value, match)
@last_failure = "no error matching #{match.inspect} when value is #{format_value(value)}" unless pass
def no_error_matching(value, option)
pass = !error_matching(value, option)
unless pass
error = error_message_for(option)
@last_failure = "no error matching '#{error}' when value is #{format_value(value)}"
end
pass
end
def error_message_for(option)
msg = @validator.send(:error_messages)[option]
restriction = @validator.send(:restriction_value, @validator.configuration[option], @record)
if restriction
restriction = [restriction] unless restriction.is_a?(Array)
restriction.map! {|r| @validator.send(:type_cast_value, r) }
interpolate = @validator.send(:interpolation_values, option, restriction )
# 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
end
end
msg
end
def format_value(value)
return value if value.is_a?(String)
value.strftime(ValidatesTimeliness::Validator.error_value_formats[options[:type]])
value.strftime(ValidatesTimeliness::Validator.error_value_formats[@type])
end
end
def validate_date(attribute, options={})
options[:type] = :date
validate_timeliness_of(attribute, options)
ValidateTimeliness.new(attribute, options)
end
def validate_time(attribute, options={})
options[:type] = :time
validate_timeliness_of(attribute, options)
ValidateTimeliness.new(attribute, options)
end
def validate_datetime(attribute, options={})
options[:type] = :datetime
validate_timeliness_of(attribute, options)
end
private
def validate_timeliness_of(attribute, options={})
ValidateTimeliness.new(attribute, options)
end
end
end
end

View File

@@ -11,6 +11,14 @@ module ValidatesTimeliness
:datetime => '%Y-%m-%d %H:%M:%S'
}
RESTRICTION_METHODS = {
:before => :<,
:after => :>,
:on_or_before => :<=,
:on_or_after => :>=,
:between => lambda {|v, r| (r.first..r.last).include?(v) }
}
attr_reader :configuration, :type
def initialize(configuration)
@@ -40,21 +48,17 @@ module ValidatesTimeliness
end
def validate_restrictions(record, attr_name, value)
restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='}
display = self.class.error_value_formats[type]
value = type_cast_value(value)
restriction_methods.each do |option, method|
RESTRICTION_METHODS.each do |option, method|
next unless restriction = configuration[option]
begin
compare = restriction_value(restriction, record)
next if compare.nil?
compare = type_cast_value(compare)
restriction = restriction_value(restriction, record)
next if restriction.nil?
restriction = type_cast_value(restriction)
unless value.send(method, compare)
add_error(record, attr_name, option, :restriction => compare.strftime(display))
unless evaluate_restriction(restriction, value, method)
add_error(record, attr_name, option, interpolation_values(option, restriction))
end
rescue
unless self.class.ignore_restriction_errors
@@ -64,15 +68,41 @@ module ValidatesTimeliness
end
end
def add_error(record, attr_name, message, interpolate={})
if Rails::VERSION::STRING < '2.2'
message = error_messages[message] if message.is_a?(Symbol)
message = message % interpolate.values unless interpolate.empty?
record.errors.add(attr_name, message)
def interpolation_values(option, restriction)
format = self.class.error_value_formats[type]
restriction = [restriction] unless restriction.is_a?(Array)
if defined?(I18n)
message = custom_error_messages[option] || I18n.translate('activerecord.errors.messages')[option]
subs = message.scan(/\{\{([^\}]*)\}\}/)
interpolations = {}
subs.each_with_index {|s, i| interpolations[s[0].to_sym] = restriction[i].strftime(format) }
interpolations
else
restriction.map {|r| r.strftime(format) }
end
end
def evaluate_restriction(restriction, value, comparator)
return true if restriction.nil?
case comparator
when Symbol
value.send(comparator, restriction)
when Proc
comparator.call(value, restriction)
end
end
def add_error(record, attr_name, message, interpolate=nil)
if defined?(I18n)
# use i18n support in AR for message or use custom message passed to validation method
custom = custom_error_messages[message]
record.errors.add(attr_name, custom || message, interpolate)
record.errors.add(attr_name, custom || message, interpolate || {})
else
message = error_messages[message] if message.is_a?(Symbol)
message = message % interpolate
record.errors.add(attr_name, message)
end
end
@@ -83,7 +113,12 @@ module ValidatesTimeliness
def custom_error_messages
return @custom_error_messages if defined?(@custom_error_messages)
@custom_error_messages = configuration.inject({}) {|h, (k, v)| h[$1.to_sym] = v if k.to_s =~ /(.*)_message$/;h }
@custom_error_messages = configuration.inject({}) {|msgs, (k, v)|
if md = /(.*)_message$/.match(k.to_s)
msgs[md[1].to_sym] = v
end
msgs
}
end
def restriction_value(restriction, record)
@@ -94,12 +129,19 @@ module ValidatesTimeliness
restriction_value(record.send(restriction), record)
when Proc
restriction_value(restriction.call(record), record)
when Array
restriction.map {|r| restriction_value(r, record) }.sort
when Range
restriction_value([restriction.first, restriction.last], record)
else
record.class.parse_date_time(restriction, type, false)
end
end
def type_cast_value(value)
if value.is_a?(Array)
value.map {|v| type_cast_value(v) }
else
case type
when :time
value.to_dummy_time
@@ -109,12 +151,13 @@ module ValidatesTimeliness
if value.is_a?(DateTime) || value.is_a?(Time)
value.to_time
else
value.to_time(ValidatesTimelines.default_timezone)
value.to_time(ValidatesTimeliness.default_timezone)
end
else
nil
end
end
end
end
end

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

@@ -163,6 +163,11 @@ describe ValidatesTimeliness::Formats do
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.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.should == [0,0,0,12,12,0,0]

View File

@@ -5,42 +5,59 @@ 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
attr_accessor :no_validation, :with_validation
@@attribute_for_type = { :date => :birth_date, :time => :birth_time, :datetime => :birth_date_and_time }
before do
@no_validation = NoValidation.new
@with_validation = WithValidation.new
end
[:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do
with_validation.should self.send("validate_#{type}", "birth_#{attribute}".to_sym)
with_validation.should self.send("validate_#{type}", attribute_for_type(type))
end
it "should report that #{type} is not validated" do
no_validation.should_not self.send("validate_#{type}", "birth_#{attribute}".to_sym)
no_validation.should_not self.send("validate_#{type}", attribute_for_type(type))
end
end
@@ -52,18 +69,17 @@ describe "ValidateTimeliness matcher" do
}
[:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do
with_validation.should self.send("validate_#{type}", "birth_#{attribute}", :before => test_values[type][0])
with_validation.should self.send("validate_#{type}", attribute_for_type(type), :before => test_values[type][0])
end
it "should report that #{type} is not validated when option value is incorrect" do
with_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :before => test_values[type][1])
with_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :before => test_values[type][1])
end
it "should report that #{type} is not validated with option" do
no_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :before => test_values[type][0])
no_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :before => test_values[type][0])
end
end
end
@@ -76,18 +92,17 @@ describe "ValidateTimeliness matcher" do
}
[:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do
with_validation.should self.send("validate_#{type}", "birth_#{attribute}", :after => test_values[type][0])
with_validation.should self.send("validate_#{type}", attribute_for_type(type), :after => test_values[type][0])
end
it "should report that #{type} is not validated when option value is incorrect" do
with_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :after => test_values[type][1])
with_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :after => test_values[type][1])
end
it "should report that #{type} is not validated with option" do
no_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :after => test_values[type][0])
no_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :after => test_values[type][0])
end
end
end
@@ -100,18 +115,17 @@ describe "ValidateTimeliness matcher" do
}
[:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do
with_validation.should self.send("validate_#{type}", "birth_#{attribute}", :on_or_before => test_values[type][0])
with_validation.should self.send("validate_#{type}", attribute_for_type(type), :on_or_before => test_values[type][0])
end
it "should report that #{type} is not validated when option value is incorrect" do
with_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :on_or_before => test_values[type][1])
with_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :on_or_before => test_values[type][1])
end
it "should report that #{type} is not validated with option" do
no_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :on_or_before => test_values[type][0])
no_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :on_or_before => test_values[type][0])
end
end
end
@@ -124,18 +138,40 @@ describe "ValidateTimeliness matcher" do
}
[:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do
with_validation.should self.send("validate_#{type}", "birth_#{attribute}", :on_or_after => test_values[type][0])
with_validation.should self.send("validate_#{type}", attribute_for_type(type), :on_or_after => test_values[type][0])
end
it "should report that #{type} is not validated when option value is incorrect" do
with_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :on_or_after => test_values[type][1])
with_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :on_or_after => test_values[type][1])
end
it "should report that #{type} is not validated with option" do
no_validation.should_not self.send("validate_#{type}", "birth_#{attribute}", :on_or_after => test_values[type][0])
no_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :on_or_after => test_values[type][0])
end
end
end
describe "between option" do
test_values = {
:date => [ ['2000-01-01', '2000-01-03'], ['2000-01-01', '2000-01-04'] ],
:time => [ ['09:00', '17:00'], ['09:00', '17:01'] ],
:datetime => [ ['2000-01-01 09:00', '2000-01-01 17:00'], ['2000-01-01 09:00', '2000-01-01 17:01'] ]
}
[:date, :time, :datetime].each do |type|
it "should report that #{type} is validated" do
with_validation.should self.send("validate_#{type}", attribute_for_type(type), :between => test_values[type][0])
end
it "should report that #{type} is not validated when option value is incorrect" do
with_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :between => test_values[type][1])
end
it "should report that #{type} is not validated with option" do
no_validation.should_not self.send("validate_#{type}", attribute_for_type(type), :between => test_values[type][0])
end
end
end
@@ -175,4 +211,8 @@ describe "ValidateTimeliness matcher" do
end
end
def attribute_for_type(type)
@@attribute_for_type[type.to_sym]
end
end

View File

@@ -43,6 +43,25 @@ describe ValidatesTimeliness::Validator do
restriction_value(lambda {"2007-01-01 12:00"}, :datetime).should be_kind_of(Time)
end
it "should return array of Time objects when restriction is array of Time objects" do
time1, time2 = Time.now, 1.day.ago
restriction_value([time1, time2], :datetime).should == [time2, time1]
end
it "should return array of Time objects when restriction is array of strings" do
time1, time2 = "2000-01-02", "2000-01-01"
restriction_value([time1, time2], :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
end
it "should return array of Time objects when restriction is Range of Time objects" do
time1, time2 = Time.now, 1.day.ago
restriction_value(time1..time2, :datetime).should == [time2, time1]
end
it "should return array of Time objects when restriction is Range of time strings" do
time1, time2 = "2000-01-02", "2000-01-01"
restriction_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
end
def restriction_value(restriction, type)
configure_validator(:type => type)
validator.send(:restriction_value, restriction, person)
@@ -212,83 +231,101 @@ describe ValidatesTimeliness::Validator do
end
end
describe "instance with on_or_before and on_or_after restrictions" do
describe "instance with between restriction" do
describe "for datetime type" do
before do
configure_validator(:on_or_before => Time.now.at_midnight, :on_or_after => 1.day.ago)
configure_validator(:between => [1.day.ago.at_midnight, 1.day.from_now.at_midnight])
end
it "should have error when value is past :on_or_before restriction" do
validate_with(:birth_date_and_time, Time.now.at_midnight + 1)
should_have_error(:birth_date_and_time, :on_of_before)
it "should have error when value is before earlist :between restriction" do
validate_with(:birth_date_and_time, 2.days.ago)
should_have_error(:birth_date_and_time, :between)
end
it "should be valid when value is equal to :on_or_before restriction" do
validate_with(:birth_date_and_time, Time.now.at_midnight)
should_have_no_error(:birth_date_and_time, :on_of_before)
it "should have error when value is after latest :between restriction" do
validate_with(:birth_date_and_time, 2.days.from_now)
should_have_error(:birth_date_and_time, :between)
end
it "should have error when value is before :on_or_after restriction" do
validate_with(:birth_date_and_time, 1.days.ago - 1)
should_have_error(:birth_date_and_time, :on_of_after)
it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_date_and_time, 1.day.ago.at_midnight)
should_have_no_error(:birth_date_and_time, :between)
end
it "should be valid when value is value equal to :on_or_after restriction" do
validate_with(:birth_date_and_time, 1.day.ago)
should_have_no_error(:birth_date_and_time, :on_of_after)
it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_date_and_time, 1.day.from_now.at_midnight)
should_have_no_error(:birth_date_and_time, :between)
end
it "should allow a range for between restriction" do
configure_validator(:type => :datetime, :between => (1.day.ago.at_midnight)..(1.day.from_now.at_midnight))
validate_with(:birth_date_and_time, 1.day.from_now.at_midnight)
should_have_no_error(:birth_date_and_time, :between)
end
end
describe "for date type" do
before :each do
configure_validator(:on_or_before => 1.day.from_now, :on_or_after => 1.day.ago, :type => :date)
before do
configure_validator(:type => :date, :between => [1.day.ago.to_date, 1.day.from_now.to_date])
end
it "should have error when value is past :on_or_before restriction" do
validate_with(:birth_date, 2.days.from_now)
should_have_error(:birth_date, :on_or_before)
it "should have error when value is before earlist :between restriction" do
validate_with(:birth_date, 2.days.ago.to_date)
should_have_error(:birth_date, :between)
end
it "should have error when value is before :on_or_after restriction" do
validate_with(:birth_date, 2.days.ago)
should_have_error(:birth_date, :on_or_after)
it "should have error when value is after latest :between restriction" do
validate_with(:birth_date, 2.days.from_now.to_date)
should_have_error(:birth_date, :between)
end
it "should be valid when value is equal to :on_or_before restriction" do
validate_with(:birth_date, 1.day.from_now)
should_have_no_error(:birth_date, :on_or_before)
it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_date, 1.day.ago.to_date)
should_have_no_error(:birth_date, :between)
end
it "should be valid when value value is equal to :on_or_after restriction" do
validate_with(:birth_date, 1.day.ago)
should_have_no_error(:birth_date, :on_or_before)
it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_date, 1.day.from_now.to_date)
should_have_no_error(:birth_date, :between)
end
it "should allow a range for between restriction" do
configure_validator(:type => :date, :between => (1.day.ago.to_date)..(1.day.from_now.to_date))
validate_with(:birth_date, 1.day.from_now.to_date)
should_have_no_error(:birth_date, :between)
end
end
describe "for time type" do
before :each do
configure_validator(:on_or_before => "23:00", :on_or_after => "06:00", :type => :time)
before do
configure_validator(:type => :time, :between => ["09:00", "17:00"])
end
it "should have error when value is past :on_or_before restriction" do
validate_with(:birth_time, "23:01")
should_have_error(:birth_time, :on_or_before)
it "should have error when value is before earlist :between restriction" do
validate_with(:birth_time, "08:59")
should_have_error(:birth_time, :between)
end
it "should have error when value is before :on_or_after restriction" do
validate_with(:birth_time, "05:59")
should_have_error(:birth_time, :on_or_after)
it "should have error when value is after latest :between restriction" do
validate_with(:birth_time, "17:01")
should_have_error(:birth_time, :between)
end
it "should be valid when value is on boundary of :on_or_before restriction" do
validate_with(:birth_time, "23:00")
should_have_no_error(:birth_time, :on_or_before)
it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_time, "09:00")
should_have_no_error(:birth_time, :between)
end
it "should be valid when value is on boundary of :on_or_after restriction" do
validate_with(:birth_time, "06:00")
should_have_no_error(:birth_time, :on_or_after)
it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_time, "17:00")
should_have_no_error(:birth_time, :between)
end
it "should allow a range for between restriction" do
configure_validator(:type => :time, :between => "09:00".."17:00")
validate_with(:birth_time, "17:00")
should_have_no_error(:birth_time, :between)
end
end
end
@@ -326,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 })
@@ -433,6 +504,6 @@ describe ValidatesTimeliness::Validator do
def error_messages
return @error_messages if defined?(@error_messages)
messages = validator.send(:error_messages)
@error_messages = messages.inject({}) {|h, (k, v)| h[k] = v.gsub(/ (\%s|\{\{\w*\}\})/, ''); h }
@error_messages = messages.inject({}) {|h, (k, v)| h[k] = v.sub(/ (\%s|\{\{\w*\}\}).*/, ''); h }
end
end

View File

@@ -2,12 +2,12 @@
Gem::Specification.new do |s|
s.name = %q{validates_timeliness}
s.version = "1.0.0"
s.version = "1.1.5"
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{2008-12-07}
s.date = %q{2009-01-21}
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"]