Compare commits

...

66 Commits
1.1.2 ... 2.0.0

Author SHA1 Message Date
Adam Meehan
af3a3b84ab version 2.0.0 2009-04-12 11:13:52 +10:00
Adam Meehan
4df63c7142 another idea in TODO 2009-04-12 11:10:58 +10:00
Adam Meehan
26066dedfe update TODOs 2009-04-12 10:26:31 +10:00
Adam Meehan
3348ca3063 dont rely on add_error return value to exit. sigh, less one liners 2009-04-12 10:23:09 +10:00
Adam Meehan
88ee33ae41 consistently use instance methods vs vars in validator 2009-04-12 10:17:33 +10:00
Adam Meehan
51afb2852e update benchmark file 2009-04-12 09:44:46 +10:00
Adam Meehan
57b8a52f07 add :format option to readme 2009-04-10 11:01:19 +10:00
Adam Meehan
bb94e234bc add :format option to validate and parse with specific a format 2009-04-10 10:57:27 +10:00
Adam Meehan
f041524124 add equal_to option to matcher 2009-04-10 10:48:24 +10:00
Adam Meehan
e1d23d0f2b Merge branch '2.0.0' 2009-04-09 18:54:34 +10:00
Adam Meehan
613001791a limit time_array size explicity 2009-04-09 18:53:28 +10:00
Adam Meehan
6d12790d2b datetime select helper extension activation in readme 2009-04-06 17:34:23 +10:00
Adam Meehan
b197d537f1 fix read attribute bug for frozen record (reported by Les Nightingill) 2009-04-06 13:52:07 +10:00
Adam Meehan
1e3c802031 capture zone offset value in formats to possible usage 2009-03-31 21:35:49 +11:00
Adam Meehan
d89266d9f1 minor stuff 2009-03-31 21:25:05 +11:00
Adam Meehan
9e2d95c3e1 remove brittle and not very useful specs, which are covered elsewhere 2009-03-31 14:22:13 +11:00
Adam Meehan
fb520bbddc use plugin parser in action view extension 2009-03-31 14:21:07 +11:00
Adam Meehan
8303be05c3 little spec consistency 2009-03-30 21:03:51 +11:00
Adam Meehan
f4ed751c26 changed back to using error_value_formats for Rails 2.0/2.1 2009-03-28 19:51:11 +11:00
Adam Meehan
956933f58b disable multiparameter values extension by default for v2 2009-03-28 18:53:47 +11:00
Adam Meehan
7967b5a212 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
2009-03-28 18:49:26 +11:00
Adam Meehan
a836ed8434 changed Formats#parse to take options hash for strict and other possibilities 2009-03-28 17:35:41 +11:00
Adam Meehan
c2a4f45b5a removed old spec 2009-03-28 17:35:21 +11:00
Adam Meehan
312c1510cb refactored AR parsing methods into Parser module to reduce AR method pollution and make more consistent 2009-03-28 17:25:48 +11:00
Adam Meehan
88fce1d679 updated version number (1.1.7) 2009-03-26 16:49:19 +11:00
Adam Meehan
ffd8476f74 version 1.1.7 2009-03-26 16:46:53 +11:00
Adam Meehan
728c5ccda5 safely extract time values from multiparam array 2009-03-26 15:20:08 +11:00
Adam Meehan
4f0c81b6f8 properly chain multiparameter attributes 2009-03-26 15:13:35 +11:00
Adam Meehan
11e643c0fe version 1.1.6 2009-03-19 20:52:35 +11:00
Adam Meehan
d1ee94248b added :equal_to and :ignore_usec options. the later is for ignore microsecond value in datetime restrictions 2009-03-19 20:49:06 +11:00
Adam Meehan
eecef62de4 updating ginger scenarios with Rails 2.3.2 2009-03-19 20:47:50 +11:00
Adam Meehan
fae38cfecd use value as raw_value if object doesn't have _before_type_cast method 2009-03-12 19:08:31 +11:00
Adam Meehan
515a1b3126 make format sections titles 2009-03-12 07:33:13 +11:00
Adam Meehan
736b1b582f removed dupe example 2009-03-12 07:30:35 +11:00
Adam Meehan
903850bc23 Merge branch 'with' for with_date and with_time options 2009-03-10 15:51:07 +11:00
Adam Meehan
c3f3edf324 added enable method for multiparameter attribute handling of datetime which is enabled by default but will be off in version2
renamed enable action view extension method and enable both extensions in the one method as they are needed together
2009-03-08 17:55:48 +11:00
Adam Meehan
1dbac5190b fix last refactor so it casts Date object to time if attribute is datetime or time with spec added 2009-03-05 19:56:38 +11:00
Adam Meehan
497a97e0b0 cleanup setup and version check guff. enable action view extension by default until version 2 2009-03-05 18:34:16 +11:00
Adam Meehan
9dd3282a81 add method to enable action view invalid value extension. will be on by default but will off in version 2 2009-03-05 18:32:25 +11:00
Adam Meehan
e7e9a8b238 fix bad variable name 2009-03-05 18:30:48 +11:00
Adam Meehan
cb962d1157 some refactoring and behave even more friendly with alias_method_chain 2009-03-05 18:10:59 +11:00
Adam Meehan
403a91addf added if and unless to valid options 2009-03-04 21:09:51 +11:00
Adam Meehan
7ca84f3116 behaves as better AR citizen by alias_method_chaining define_attribute_methods and read_attribute instead of overriding 2009-03-03 12:38:54 +11:00
Adam Meehan
19457a6c1d removed some module inclusion silliness in spec 2009-02-27 23:40:14 +11:00
Adam Meehan
5e85649a34 readme plugin installation fix and gem dependency 2009-02-26 09:26:16 +11:00
Adam Meehan
e76c53a295 doc fix 2009-02-09 18:20:29 +11:00
Adam Meehan
f93720177b doc fixes 2009-02-09 18:14:51 +11:00
Adam Meehan
1181b725d0 doc tweaks 2009-02-09 18:12:08 +11:00
Adam Meehan
12aa78271e added docs and removed with_* from todo 2009-02-09 17:49:39 +11:00
Adam Meehan
862b41f903 added :with_date and :with_time options
refactored restriction_value into evaluate_option_value class method for more general usage
refactored type_cast_value into class method
2009-02-09 16:44:03 +11:00
Adam Meehan
904c202fb4 little cleanup and consistency 2009-02-09 12:11:56 +11:00
Adam Meehan
1001d29c01 rails 2.3.0 fix with I18n.reload!
added 2.3 to ginger list
2009-02-08 20:54:45 +11:00
Adam Meehan
29c23a7a26 move format compilcation call to where it belongs 2009-02-08 13:02:06 +11:00
Adam Meehan
7ef9078369 use the value from validates_each in validator 2009-02-08 12:46:22 +11:00
Adam Meehan
a1ae5f9313 added option key validation to prevent silly validation problems due to bad key name 2009-02-01 20:08:07 +11:00
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
23 changed files with 796 additions and 489 deletions

View File

@@ -1,3 +1,33 @@
= 2.0.0 [2009-04-12]
- Error value formats are now specified in the i18n locale file instead of updating plugin hash. See OTHER CUSTOMISATION section in README.
- Date/time select helper extension is disabled by default. To enable see DISPLAY INVALID VALUES IN DATE HELPERS section in README to enable.
- Added :format option to limit validation to a single format if desired
- Matcher now supports :equal_to option
- Formats.parse can take :include_offset option to include offset value from string in seconds, if string contains an offset. Offset not used in rest of plugin yet.
- Refactored to remove as much plugin code from ActiveRecord as possible.
= 1.1.7 [2009-03-26]
- Minor change to multiparameter attributes which I had not properly implemented for chaining
= 1.1.6 [2009-03-19]
- Rail 2.3 support
- Added :with_date and :with_time options. They allow an attribute to be combined with another attribute or value to make a datetime value for validation against the temporal restrictions
- Added :equal_to option
- Option key validation
- Better behaviour with other plugins using alias_method_chain on read_attribute and define_attribute_methods
- Added option to enable datetime_select extension for future use to optionally enable. Enabled by default until version 2.
- Added :ignore_usec option for datetime restrictions to be compared without microsecond
- some refactoring
= 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

View File

@@ -33,12 +33,16 @@ think should be a valid date or time string.
As plugin (from master)
./script/plugin git://github.com/adzap/validates_timeliness.git
./script/plugin install git://github.com/adzap/validates_timeliness.git
As gem
sudo gem install validates_timeliness
# in environment.rb
config.gem 'validates_timeliness'
== USAGE:
@@ -51,50 +55,55 @@ validation method
end
The list of validation methods available are as follows:
* validates_date - validate value as date
* validates_time - validate value as time only i.e. '12:20pm'
* validates_datetime - validate value as a full date and time
validates_date - validate value as date
validates_time - validate value as time only i.e. '12:20pm'
validates_datetime - validate value as a full date and time
The validation methods take the usual options plus some specific ones to restrict
the valid range of dates or times allowed
Temporal options (or restrictions):
:before - Attribute must be before this value to be valid
: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
Temporal options (or restrictions):
:equal_to - Attribute must be equal to value to be valid
:before - Attribute must be before this value to be valid
: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. Takes an array of two values or a range
Regular validation options:
:allow_nil - Allow a nil value to be valid
:allow_blank - Allows a nil or empty string value to be valid
:if - Execute validation when :if evaluates true
:unless - Execute validation when :unless evaluates false
Regular validation options:
:allow_nil - Allow a nil value to be valid
:allow_blank - Allows a nil or empty string value to be valid
:if - Execute validation when :if evaluates true
:unless - Execute validation when :unless evaluates false
Message options: - Use these to override the default error messages
:invalid_date_message
:invalid_time_message
:invalid_datetime_message
:before_message
:on_or_before_message
:after_message
:on_or_after_message
:between_message
Special options:
:with_time - Validate a date attribute value combined with a time value against any temporal restrictions
:with_date - Validate a time attribute value combined with a date value against any temporal restrictions
:ignore_usec - Ignores microsecond value on datetime restrictions
:format - Limit validation to a single format for special cases. Takes plugin format value.
Message options: - Use these to override the default error messages
:invalid_date_message
:invalid_time_message
:invalid_datetime_message
:equal_to_message
:before_message
:on_or_before_message
:after_message
:on_or_after_message
:between_message
The temporal restrictions can take 4 different value types:
* String value
* 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
The temporal restrictions, with_date and with_time can take 4 different value types:
* String value
* Date, Time, or DateTime object value
* Proc or lambda object which may take an optional parameter being the record object
* A symbol matching the method name in the model
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
values are compared as dates.
values are compared as dates. This is except in the case of with_time and with_date
options which effectively force the value to validated as a datetime against the
temporal options.
== EXAMPLES:
@@ -107,6 +116,8 @@ values are compared as dates.
:allow_nil => true
validates_datetime :appointment_date, :before => Proc.new { 1.week.from_now }
validates_date :entry_date, :with_time => '17:00', :on_or_before => :competition_closing
=== DATE/TIME FORMATS:
@@ -120,44 +131,43 @@ be happy to know that is exactly the format you can use to define your own if
you want. No complex regular expressions or duck punching (monkey patching) the
plugin is needed.
Time formats:
hh:nn:ss
hh-nn-ss
h:nn
h.nn
h nn
h-nn
h:nn_ampm
h.nn_ampm
h nn_ampm
h-nn_ampm
h_ampm
NOTE: Any time format without a meridian token (the 'ampm' token) is considered
in 24 hour time.
Date formats:
yyyy/mm/dd
yyyy-mm-dd
yyyy.mm.dd
m/d/yy OR d/m/yy
m\d\yy OR d\m\yy
d-m-yy
d.m.yy
d mmm yy
NOTE: To use non-US date formats see US/EURO FORMATS section
Datetime formats:
m/d/yy h:nn:ss OR d/m/yy hh:nn:ss
m/d/yy h:nn OR d/m/yy h:nn
m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm
yyyy-mm-dd hh:nn:ss
yyyy-mm-dd h:nn
ddd mmm d hh:nn:ss zo yyyy # Ruby time string
yyyy-mm-ddThh:nn:ss(?:Z|zo) # ISO 8601
==== Time formats:
hh:nn:ss
hh-nn-ss
h:nn
h.nn
h nn
h-nn
h:nn_ampm
h.nn_ampm
h nn_ampm
h-nn_ampm
h_ampm
NOTE: To use non-US date formats see US/EURO FORMATS section
NOTE: Any time format without a meridian token (the 'ampm' token) is considered in 24 hour time.
==== Date formats:
yyyy/mm/dd
yyyy-mm-dd
yyyy.mm.dd
m/d/yy OR d/m/yy
m\d\yy OR d\m\yy
d-m-yy
d.m.yy
d mmm yy
NOTE: To use non-US date formats see US/EURO FORMATS section
==== Datetime formats:
m/d/yy h:nn:ss OR d/m/yy hh:nn:ss
m/d/yy h:nn OR d/m/yy h:nn
m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm
yyyy-mm-dd hh:nn:ss
yyyy-mm-dd h:nn
ddd mmm d hh:nn:ss zo yyyy # Ruby time string
yyyy-mm-ddThh:nn:ss(?:Z|zo) # ISO 8601
NOTE: To use non-US date formats see US/EURO FORMATS section
Here is what each format token means:
@@ -219,7 +229,7 @@ Done! That format is no longer considered valid. Easy!
Ok, now I hear you say "Well I have format that I want to use but you don't have it".
Ahh, then add it yourself. Again stick this in an initializer file
ValidatesTimeliness::Formats.add_formats(:time, "d o'clock")
ValidatesTimeliness::Formats.add_formats(:time, "d o'clock")
Now "10 o'clock" will be a valid value. So easy, no more whingeing!
@@ -234,7 +244,7 @@ with an existing format, will mean your format is ignored. If you need to make
your new format higher precedence than an existing format, you can include the
before option like so
ValidatesTimeliness::Formats.add_formats(:time, 'ss:nn:hh', :before => 'hh:nn:ss')
ValidatesTimeliness::Formats.add_formats(:time, 'ss:nn:hh', :before => 'hh:nn:ss')
Now a time of '59:30:23' will be interpreted as 11:30:59 pm. This option saves
you adding a new one and deleting an old one to get it to work.
@@ -257,6 +267,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
@@ -293,12 +317,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
For Rails 2.0/2.1:
ValidatesTimeliness::Validator.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.

View File

@@ -5,7 +5,7 @@ require 'date'
require 'spec/rake/spectask'
GEM = "validates_timeliness"
GEM_VERSION = "1.1.2"
GEM_VERSION = "2.0.0"
AUTHOR = "Adam Meehan"
EMAIL = "adam.meehan@gmail.com"
HOMEPAGE = "http://github.com/adzap/validates_timeliness"
@@ -24,9 +24,6 @@ spec = Gem::Specification.new do |s|
s.email = EMAIL
s.homepage = HOMEPAGE
# Uncomment this to add a dependency
# s.add_dependency "foo"
s.require_path = 'lib'
s.autorequire = GEM
s.files = %w(LICENSE README.rdoc Rakefile TODO CHANGELOG) + Dir.glob("{lib,spec}/**/*")

6
TODO
View File

@@ -1,5 +1,5 @@
- :format option
- :with_date and :with_time options
- valid formats could come from locale file
- formats to use month and day names from i18n
- add replace_formats instead add_formats :before
- array of values for all temporal options
- use tz and zo value from time string?
- filter valid formats rather than remove for hot swapping without recompilation

View File

@@ -4,73 +4,74 @@ require 'date'
require 'parsedate'
require 'benchmark'
require 'rubygems'
require 'active_support'
require 'active_record'
require 'action_controller'
require 'rails/version'
require 'validates_timeliness'
def parse(*args)
ValidatesTimeliness::Parser.parse(*args)
end
n = 10000
Benchmark.bm do |x|
x.report('timeliness - datetime') {
n.times do
ActiveRecord::Base.parse_date_time("2000-01-04 12:12:12", :datetime)
parse("2000-01-04 12:12:12", :datetime)
end
}
x.report('timeliness - date') {
n.times do
ActiveRecord::Base.parse_date_time("2000-01-04", :date)
parse("2000-01-04", :date)
end
}
x.report('timeliness - date as datetime') {
n.times do
ActiveRecord::Base.parse_date_time("2000-01-04", :datetime)
parse("2000-01-04", :datetime)
end
}
x.report('timeliness - time') {
n.times do
ActiveRecord::Base.parse_date_time("12:01:02", :time)
parse("12:01:02", :time)
end
}
x.report('timeliness - invalid format datetime') {
n.times do
ActiveRecord::Base.parse_date_time("20xx-01-04 12:12:12", :datetime)
parse("20xx-01-04 12:12:12", :datetime)
end
}
x.report('timeliness - invalid format date') {
n.times do
ActiveRecord::Base.parse_date_time("20xx-01-04", :date)
parse("20xx-01-04", :date)
end
}
x.report('timeliness - invalid format time') {
n.times do
ActiveRecord::Base.parse_date_time("12:xx:02", :time)
parse("12:xx:02", :time)
end
}
x.report('timeliness - invalid value datetime') {
n.times do
ActiveRecord::Base.parse_date_time("2000-01-32 12:12:12", :datetime)
parse("2000-01-32 12:12:12", :datetime)
end
}
x.report('timeliness - invalid value date') {
n.times do
ActiveRecord::Base.parse_date_time("2000-01-32", :date)
parse("2000-01-32", :date)
end
}
x.report('timeliness - invalid value time') {
n.times do
ActiveRecord::Base.parse_date_time("12:61:02", :time)
parse("12:61:02", :time)
end
}
x.report('date/time') {
@@ -96,4 +97,3 @@ Benchmark.bm do |x|
end
}
end

View File

@@ -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,54 +15,39 @@ 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
def load_error_messages_with_i18n
I18n.load_path += [ LOCALE_PATH ]
def enable_datetime_select_extension!
enable_datetime_select_invalid_value_extension!
enable_multiparameter_attributes_extension!
end
def load_error_messages_without_i18n
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 }
::ActiveRecord::Errors.default_error_messages.update(errors)
end
def default_error_messages
if Rails::VERSION::STRING < '2.2'
::ActiveRecord::Errors.default_error_messages
def load_error_messages
if defined?(I18n)
I18n.load_path += [ LOCALE_PATH ]
I18n.reload!
else
I18n.translate('activerecord.errors.messages')
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.error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys
end
end
def setup_for_rails_2_0
load_error_messages_without_i18n
end
def setup_for_rails_2_1
load_error_messages_without_i18n
end
def setup_for_rails_2_2
load_error_messages_with_i18n
end
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
rescue
puts "Rails version #{Rails::VERSION::STRING} not explicitly supported by validates_timeliness plugin. You may encounter some problems."
resume
self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
load_error_messages
end
end
end
ValidatesTimeliness.setup_for_rails
ValidatesTimeliness::Formats.compile_format_expressions

View File

@@ -1,4 +1,9 @@
module ValidatesTimeliness
def self.enable_datetime_select_invalid_value_extension!
::ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::ActionView::InstanceTag)
end
module ActionView
# Intercepts the date and time select helpers to allow the
@@ -32,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
@@ -41,5 +46,3 @@ module ValidatesTimeliness
end
end
ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::ActionView::InstanceTag)

View File

@@ -14,22 +14,14 @@ 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
base.class_eval do
alias_method_chain :read_attribute, :timeliness
class << self
alias_method_chain :define_attribute_methods, :timeliness
end
end
end
@@ -38,110 +30,92 @@ module ValidatesTimeliness
# and value can be used from cache. This prevents the raw date/time value from
# being type cast using default Rails type casting when writing values
# to the database.
def read_attribute(attr_name)
def read_attribute_with_timeliness(attr_name)
attr_name = attr_name.to_s
if !(value = @attributes[attr_name]).nil?
if column = column_for_attribute(attr_name)
if unserializable_attribute?(attr_name, column)
unserialize_attribute(attr_name)
elsif [:date, :time, :datetime].include?(column.type) && @attributes_cache.has_key?(attr_name)
@attributes_cache[attr_name]
else
column.type_cast(value)
end
else
value
column = column_for_attribute(attr_name)
if column && [:date, :time, :datetime].include?(column.type) && @attributes_cache.has_key?(attr_name)
return @attributes_cache[attr_name]
end
else
nil
end
read_attribute_without_timeliness(attr_name)
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)
old = read_attribute(attr_name) if defined?(::ActiveRecord::Dirty)
new = self.class.parse_date_time(value, column.type)
def write_date_time_attribute(attr_name, value, type, time_zone_aware)
new = ValidatesTimeliness::Parser.parse(value, type)
unless column.type == :date || new.nil?
new = new.to_time rescue new
if new && type != :date
new = new.to_time
new = new.in_time_zone if time_zone_aware
end
if self.class.send(:create_time_zone_conversion_attribute?, attr_name, column)
new = new.in_time_zone rescue nil
if defined?(::ActiveRecord::Dirty) && !changed_attributes.include?(attr_name)
old = read_attribute(attr_name)
if old != new
changed_attributes[attr_name] = (old.duplicable? ? old.clone : old)
end
end
@attributes_cache[attr_name] = new
if defined?(::ActiveRecord::Dirty) && !changed_attributes.include?(attr_name) && old != new
changed_attributes[attr_name] = (old.duplicable? ? old.clone : old)
end
@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 = ValidatesTimeliness::Parser.parse(time, type)
else
time = read_attribute(attr_name)
@attributes[attr_name] = (time && time_zone_aware ? time.in_time_zone : time) unless frozen?
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
# date, time and datetime attributes to use plugin parser.
def define_attribute_methods
def define_attribute_methods_with_timeliness
return if generated_methods?
columns_hash.each do |name, column|
unless instance_method_already_implemented?(name)
if self.serialized_attributes[name]
define_read_method_for_serialized_attribute(name)
elsif create_time_zone_conversion_attribute?(name, column)
define_read_method_for_time_zone_conversion(name)
else
define_read_method(name.to_sym, name, column)
if [: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)
end
end
unless instance_method_already_implemented?("#{name}=")
if [:date, :time, :datetime].include?(column.type)
define_write_method_for_dates_and_times(name)
else
define_write_method(name.to_sym)
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)
end
end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end
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)
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,6 +1,10 @@
module ValidatesTimeliness
module ActiveRecord
def self.enable_multiparameter_attributes_extension!
::ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes)
end
module ActiveRecord
module MultiparameterAttributes
def self.included(base)
@@ -12,18 +16,16 @@ module ValidatesTimeliness
def execute_callstack_for_multiparameter_attributes_with_timeliness(callstack)
errors = []
callstack.each do |name, values|
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
if values.empty?
send(name + "=", nil)
else
column = column_for_attribute(name)
column = column_for_attribute(name)
if column && [:date, :time, :datetime].include?(column.type)
begin
value = if [:date, :time, :datetime].include?(column.type)
time_array_to_string(values, column.type)
callstack.delete(name)
if values.empty?
send("#{name}=", nil)
else
klass.new(*values)
value = time_array_to_string(values, column.type)
send("#{name}=", value)
end
send(name + "=", value)
rescue => ex
errors << ::ActiveRecord::AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
end
@@ -32,19 +34,20 @@ module ValidatesTimeliness
unless errors.empty?
raise ::ActiveRecord::MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
end
execute_callstack_for_multiparameter_attributes_without_timeliness(callstack)
end
def time_array_to_string(values, type)
values = values.map(&:to_s)
values.collect! {|v| v.to_s }
case type
when :date
extract_date_from_multiparameter_attributes(values)
when :time
extract_time_from_multiparameter_attributes(values)
when :datetime
date_values, time_values = values.slice!(0, 3), values
extract_date_from_multiparameter_attributes(date_values) + " " + extract_time_from_multiparameter_attributes(time_values)
when :date
extract_date_from_multiparameter_attributes(values)
when :time
extract_time_from_multiparameter_attributes(values)
when :datetime
date_values, time_values = values.slice!(0, 3), values
extract_date_from_multiparameter_attributes(date_values) + " " + extract_time_from_multiparameter_attributes(time_values)
end
end
@@ -53,12 +56,11 @@ module ValidatesTimeliness
end
def extract_time_from_multiparameter_attributes(values)
values.last(3).map { |s| s.rjust(2, "0") }.join(":")
values = values.size > 3 ? values[3..5] : values
values.map { |s| s.rjust(2, "0") }.join(":")
end
end
end
end
ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes)

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 ] },
@@ -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.
@@ -139,13 +139,14 @@ module ValidatesTimeliness
# should just be the arg name.
#
@@format_proc_args = {
:year => [0, 'y', 'unambiguous_year(y)'],
:month => [1, 'm', 'month_index(m)'],
:day => [2, 'd', 'd'],
:hour => [3, 'h', 'full_hour(h,md)'],
:min => [4, 'n', 'n'],
:sec => [5, 's', 's'],
:usec => [6, 'u', 'microseconds(u)'],
:year => [0, 'y', 'unambiguous_year(y)'],
:month => [1, 'm', 'month_index(m)'],
:day => [2, 'd', 'd'],
:hour => [3, 'h', 'full_hour(h,md)'],
:min => [4, 'n', 'n'],
:sec => [5, 's', 's'],
:usec => [6, 'u', 'microseconds(u)'],
:offset => [7, 'z', 'offset_in_seconds(z)'],
:meridian => [nil, 'md', nil]
}
@@ -160,20 +161,29 @@ module ValidatesTimeliness
# Loop through format expressions for type and call proc on matches. Allow
# 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)
# Returns time array if matches a format, nil otherwise.
def parse(string, type, options={})
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
end
options.reverse_merge!(:strict => true)
sets = if options[:format]
[ send("#{type}_expressions").assoc(options[:format]) ]
else
expression_set(type, string)
end
return time_array
matches = nil
processor = sets.each do |format, regexp, proc|
full = /\A#{regexp}\Z/ if options[:strict]
full ||= case type
when :date then /\A#{regexp}/
when :time then /#{regexp}\Z/
when :datetime then /\A#{regexp}\Z/
end
break(proc) if matches = full.match(string.strip)
end
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.
@@ -205,8 +215,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.
@@ -223,7 +232,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
@@ -245,39 +254,34 @@ 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
def compile_formats(formats)
formats.collect { |format| regexp, format_proc = format_expression_generator(format) }
formats.map { |format| [ format, *format_expression_generator(format) ] }
end
# Pick expression set and combine date and datetimes for
# datetime attributes to allow date string as datetime
def expression_set(type, string)
case type
when :date
date_expressions
when :time
time_expressions
when :datetime
# gives a speed-up for date string as datetime attributes
if string.length < 11
date_expressions + datetime_expressions
else
datetime_expressions + date_expressions
end
when :date
date_expressions
when :time
time_expressions
when :datetime
# gives a speed-up for date string as datetime attributes
if string.length < 11
date_expressions + datetime_expressions
else
datetime_expressions + date_expressions
end
end
end
@@ -298,12 +302,29 @@ 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)
(".#{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
ValidatesTimeliness::Formats.compile_format_expressions

View File

@@ -5,8 +5,14 @@ en:
invalid_date: "is not a valid date"
invalid_time: "is not a valid time"
invalid_datetime: "is not a valid datetime"
equal_to: "must be equal to {{restriction}}"
before: "must be before {{restriction}}"
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}}"
validates_timeliness:
error_value_formats:
date: '%Y-%m-%d'
time: '%H:%M:%S'
datetime: '%Y-%m-%d %H:%M:%S'

View File

@@ -0,0 +1,46 @@
module ValidatesTimeliness
module Parser
class << self
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)
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
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
make_time(time_array[0..7])
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

View File

@@ -10,6 +10,7 @@ module Spec
}
OPTION_TEST_SETTINGS = {
:equal_to => { :method => :+, :modify_on => :invalid },
:before => { :method => :-, :modify_on => :valid },
:after => { :method => :+, :modify_on => :valid },
:on_or_before => { :method => :+, :modify_on => :invalid },
@@ -27,9 +28,9 @@ module Spec
valid = test_validity
valid = test_option(:equal_to) if @options[:equal_to] && valid
valid = test_option(:before) if @options[:before] && valid
valid = test_option(:after) if @options[: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
@@ -92,8 +93,8 @@ module Spec
end
def parse_and_cast(value)
value = @validator.send(:restriction_value, value, @record)
@validator.send(:type_cast_value, value)
value = @validator.class.send(:evaluate_option_value, value, @type, @record)
@validator.class.send(:type_cast_value, value, @type)
end
def error_matching(value, option)
@@ -116,12 +117,12 @@ module Spec
end
def error_message_for(option)
msg = @validator.send(:error_messages)[option]
restriction = @validator.send(:restriction_value, @validator.configuration[option], @record)
msg = @validator.error_messages[option]
restriction = @validator.class.send(:evaluate_option_value, @validator.configuration[option], @type, @record)
if restriction
restriction = [restriction] unless restriction.is_a?(Array)
restriction.map! {|r| @validator.send(:type_cast_value, r) }
restriction.map! {|r| @validator.class.send(:type_cast_value, r, @type) }
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)
@@ -135,7 +136,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

View File

@@ -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
@@ -49,28 +28,13 @@ module ValidatesTimeliness
private
def validates_timeliness_of(attr_names, configuration)
validator = ValidatesTimeliness::Validator.new(configuration)
validator = ValidatesTimeliness::Validator.new(configuration.symbolize_keys)
# bypass handling of allow_nil and allow_blank to validate raw value
configuration.delete(:allow_nil)
configuration.delete(:allow_blank)
validates_each(attr_names, configuration) do |record, attr_name, value|
validator.call(record, attr_name)
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
validator.call(record, attr_name, value)
end
end

View File

@@ -2,16 +2,10 @@ module ValidatesTimeliness
class Validator
cattr_accessor :ignore_restriction_errors
cattr_accessor :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 => :==,
:before => :<,
:after => :>,
:on_or_before => :<=,
@@ -19,43 +13,70 @@ module ValidatesTimeliness
:between => lambda {|v, r| (r.first..r.last).include?(v) }
}
VALID_OPTIONS = [
:on, :if, :unless, :allow_nil, :empty, :allow_blank,
:with_time, :with_date, :ignore_usec, :format,
:invalid_time_message, :invalid_date_message, :invalid_datetime_message
] + RESTRICTION_METHODS.keys.map {|option| [option, "#{option}_message".to_sym] }.flatten
attr_reader :configuration, :type
def initialize(configuration)
defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false, :ignore_usec => false }
@configuration = defaults.merge(configuration)
@type = @configuration.delete(:type)
validate_options(@configuration)
end
def call(record, attr_name)
value = record.send(attr_name)
value = record.class.parse_date_time(value, type, false) if value.is_a?(String)
raw_value = raw_value(record, attr_name)
def call(record, attr_name, value)
raw_value = raw_value(record, attr_name) || value
if value.is_a?(String) || configuration[:format]
strict = !configuration[:format].nil?
value = ValidatesTimeliness::Parser.parse(raw_value, type, :strict => strict, :format => configuration[:format])
end
return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
add_error(record, attr_name, :blank) and return if raw_value.blank?
add_error(record, attr_name, "invalid_#{type}".to_sym) and return unless value
if raw_value.blank?
add_error(record, attr_name, :blank)
return
end
if value.nil?
add_error(record, attr_name, "invalid_#{type}".to_sym)
return
end
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)
record.send("#{attr_name}_before_type_cast")
record.send("#{attr_name}_before_type_cast") rescue nil
end
def validate_restrictions(record, attr_name, value)
value = type_cast_value(value)
value = if configuration[:with_time] || configuration[:with_date]
restriction_type = :datetime
combine_date_and_time(value, record)
else
restriction_type = type
self.class.type_cast_value(value, type, configuration[:ignore_usec])
end
return if value.nil?
RESTRICTION_METHODS.each do |option, method|
next unless restriction = configuration[option]
begin
restriction = restriction_value(restriction, record)
restriction = self.class.evaluate_option_value(restriction, restriction_type, record)
next if restriction.nil?
restriction = type_cast_value(restriction)
restriction = self.class.type_cast_value(restriction, restriction_type, configuration[:ignore_usec])
unless evaluate_restriction(restriction, value, method)
add_error(record, attr_name, option, interpolation_values(option, restriction))
@@ -73,7 +94,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) }
@@ -87,16 +108,15 @@ module ValidatesTimeliness
return true if restriction.nil?
case comparator
when Symbol
value.send(comparator, restriction)
when Proc
comparator.call(value, restriction)
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 || {})
else
@@ -106,57 +126,101 @@ module ValidatesTimeliness
end
end
def error_messages
return @error_messages if defined?(@error_messages)
@error_messages = ValidatesTimeliness.default_error_messages.merge(custom_error_messages)
end
def custom_error_messages
return @custom_error_messages if defined?(@custom_error_messages)
@custom_error_messages = configuration.inject({}) {|msgs, (k, v)|
@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)
case restriction
when Time, Date, DateTime
restriction
when Symbol
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)
def combine_date_and_time(value, record)
if type == :date
date = value
time = configuration[:with_time]
else
date = configuration[:with_date]
time = value
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?
ValidatesTimeliness::Parser.make_time([date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec])
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
when :date
value.to_date
when :datetime
if value.is_a?(DateTime) || value.is_a?(Time)
value.to_time
else
value.to_time(ValidatesTimeliness.default_timezone)
end
def validate_options(options)
invalid_for_type = ([:time, :date, :datetime] - [type]).map {|k| "invalid_#{k}_message".to_sym }
invalid_for_type << :with_date unless type == :time
invalid_for_type << :with_time unless type == :date
options.assert_valid_keys(VALID_OPTIONS - invalid_for_type)
end
# class methods
class << self
def default_error_messages
if defined?(I18n)
I18n.t('activerecord.errors.messages')
else
nil
::ActiveRecord::Errors.default_error_messages
end
end
def error_value_formats
if defined?(I18n)
I18n.t('validates_timeliness.error_value_formats')
else
@@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
value
when Symbol
evaluate_option_value(record.send(value), type, record)
when Proc
evaluate_option_value(value.call(record), type, record)
when Array
value.map {|r| evaluate_option_value(r, type, record) }.sort
when Range
evaluate_option_value([value.first, value.last], type, record)
else
ValidatesTimeliness::Parser.parse(value, type, :strict => false)
end
end
def type_cast_value(value, type, ignore_usec=false)
if value.is_a?(Array)
value.map {|v| type_cast_value(v, type, ignore_usec) }
else
value = case type
when :time
value.to_dummy_time
when :date
value.to_date
when :datetime
if value.is_a?(DateTime) || value.is_a?(Time)
value.to_time
else
value.to_time(ValidatesTimeliness.default_timezone)
end
else
nil
end
if ignore_usec && value.is_a?(Time)
ValidatesTimeliness::Parser.make_time(Array(value).reverse[4..9])
else
value
end
end
end
end
end

View File

@@ -1,9 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
include ValidatesTimeliness::ActiveRecord::AttributeMethods
include ValidatesTimeliness::ValidationMethods
before do
@person = Person.new
end
@@ -23,18 +20,36 @@ 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 read_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
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
@@ -54,6 +69,11 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date_and_time.should be_kind_of(Time)
end
it "should return Time object for datetime attribute read method when assigned Date object" do
@person.birth_date_and_time = Date.today
@person.birth_date_and_time.should be_kind_of(Time)
end
it "should return Time object for datetime attribute read method when assigned string" do
@person.birth_date_and_time = "2000-01-01 02:03:04"
@person.birth_date_and_time.should be_kind_of(Time)
@@ -201,4 +221,14 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date.should == tomorrow
end
it "should skip storing value in attributes hash on read if record frozen" do
@person = Person.new
@person.birth_date = Date.today
@person.save!
@person.reload
@person.freeze
@person.frozen?.should be_true
lambda { @person.birth_date }.should_not raise_error
@person.birth_date.should == Date.today
end
end

View File

@@ -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
@@ -71,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
@@ -136,44 +100,62 @@ describe ValidatesTimeliness::Formats do
end
end
describe "extracting values" do
describe "parse" 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: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: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, 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
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
before do
formats.compile_format_expressions
describe "parse with format option" do
it "should return values if string matches specified format" do
time_array = formats.parse('2000-02-01 12:13:14', :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
time_array.should == [2000,2,1,12,13,14,0]
end
it "should return nil if string does not match specified format" do
time_array = formats.parse('2000-02-01 12:13', :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
time_array.should be_nil
end
end
describe "removing formats" do
it "should remove format from format array" do
formats.remove_formats(:time, 'h.nn_ampm')
formats.time_formats.should_not include("h o'clock")
@@ -191,7 +173,7 @@ describe ValidatesTimeliness::Formats do
after do
formats.time_formats << 'h.nn_ampm'
# reload class instead
formats.compile_format_expressions
end
end
@@ -249,7 +231,7 @@ describe ValidatesTimeliness::Formats do
def validate(time_string, type)
valid = false
formats.send("#{type}_expressions").each do |(regexp, processor)|
formats.send("#{type}_expressions").each do |format, regexp, processor|
valid = true and break if /\A#{regexp}\Z/ =~ time_string
end
valid

View File

@@ -9,7 +9,7 @@
# ginger spec
#
Ginger.configure do |config|
rails_versions = ['2.0.2', '2.1.2', '2.2.2']
rails_versions = ['2.0.2', '2.1.2', '2.2.2', '2.3.2']
rails_versions.each do |v|
g = Ginger::Scenario.new

View File

@@ -1,39 +1,39 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe ValidatesTimeliness::ValidationMethods do
describe ValidatesTimeliness::Parser do
attr_accessor :person
describe "parse_date_time" do
describe "parse" do
it "should return time object for valid time string" do
parse_method("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
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_method("2000-02-30 12:13:14", :datetime).should be_nil
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_method("2000-02-01 25:13:14", :datetime).should be_nil
parse("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)
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_method("2000-01-01 12:13:14", :datetime)
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_method("2000-02-30", :date).should be_nil
parse("2000-02-30", :date).should be_nil
end
def parse_method(*args)
ActiveRecord::Base.parse_date_time(*args)
def parse(*args)
ValidatesTimeliness::Parser.parse(*args)
end
end
@@ -43,14 +43,14 @@ describe ValidatesTimeliness::ValidationMethods do
it "should create time using current timezone" do
Time.zone = 'Melbourne'
time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0])
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 = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0])
time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0])
time.zone.should == "UTC"
end

View File

@@ -5,6 +5,7 @@ end
class WithValidation < Person
validates_date :birth_date,
:equal_to => '2000-01-01',
:before => '2000-01-10',
:after => '2000-01-01',
:on_or_before => '2000-01-09',
@@ -12,6 +13,7 @@ class WithValidation < Person
:between => ['2000-01-01', '2000-01-03']
validates_time :birth_time,
:equal_to => '09:00',
:before => '23:00',
:after => '09:00',
:on_or_before => '22:00',
@@ -19,6 +21,7 @@ class WithValidation < Person
:between => ['09:00', '17:00']
validates_datetime :birth_date_and_time,
:equal_to => '2000-01-01 09:00',
:before => '2000-01-10 23:00',
:after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00',
@@ -61,6 +64,29 @@ describe "ValidateTimeliness matcher" do
end
end
describe "with equal_to option" do
test_values = {
:date => ['2000-01-01', '2000-01-02'],
:time => ['09:00', '09:01'],
:datetime => ['2000-01-01 09:00', '2000-01-01 09: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), :equal_to => 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), :equal_to => 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), :equal_to => test_values[type][0])
end
end
end
describe "with before option" do
test_values = {
:date => ['2000-01-10', '2000-01-11'],

View File

@@ -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,15 @@ 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'
ValidatesTimeliness.enable_datetime_select_extension!
ActiveRecord::Migration.verbose = false
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})

View File

@@ -16,55 +16,71 @@ describe ValidatesTimeliness::Validator do
@person = Person.new
end
describe "restriction_value" do
describe "option keys validation" do
before do
keys = ValidatesTimeliness::Validator::VALID_OPTIONS - [:invalid_date_message, :invalid_time_message, :with_date, :with_time]
@valid_options = keys.inject({}) {|hash, opt| hash[opt] = nil; hash }
end
it "should raise error if invalid option key passed" do
@valid_options.update(:invalid_key => 'will not open lock')
lambda { Person.validates_datetime(@valid_options) }.should raise_error(ArgumentError)
end
it "should not raise error if option keys are valid" do
lambda { Person.validates_datetime(@valid_options) }.should_not raise_error(ArgumentError)
end
end
describe "evaluate_option_value" do
it "should return Time object when restriction is Time object" do
restriction_value(Time.now, :datetime).should be_kind_of(Time)
evaluate_option_value(Time.now, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is string" do
restriction_value("2007-01-01 12:00", :datetime).should be_kind_of(Time)
evaluate_option_value("2007-01-01 12:00", :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is method and method returns Time object" do
person.stub!(:datetime_attr).and_return(Time.now)
restriction_value(:datetime_attr, :datetime).should be_kind_of(Time)
evaluate_option_value(:datetime_attr, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is method and method returns string" do
person.stub!(:datetime_attr).and_return("2007-01-01 12:00")
restriction_value(:datetime_attr, :datetime).should be_kind_of(Time)
evaluate_option_value(:datetime_attr, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is proc which returns Time object" do
restriction_value(lambda { Time.now }, :datetime).should be_kind_of(Time)
evaluate_option_value(lambda { Time.now }, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is proc which returns string" do
restriction_value(lambda {"2007-01-01 12:00"}, :datetime).should be_kind_of(Time)
evaluate_option_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]
evaluate_option_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)]
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
time1, time2 = Time.now, 1.day.ago
restriction_value(time1..time2, :datetime).should == [time2, time1]
evaluate_option_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)]
evaluate_option_value(time1..time2, :datetime).should == [parse(time2, :datetime), parse(time1, :datetime)]
end
def restriction_value(restriction, type)
def evaluate_option_value(restriction, type)
configure_validator(:type => type)
validator.send(:restriction_value, restriction, person)
validator.class.send(:evaluate_option_value, restriction, type, person)
end
end
@@ -330,6 +346,100 @@ describe ValidatesTimeliness::Validator do
end
end
describe "instance with :equal_to restriction" do
describe "for datetime type" do
before do
configure_validator(:equal_to => Time.now)
end
it "should have error when value not equal to :equal_to restriction" do
validate_with(:birth_date_and_time, Time.now + 1)
should_have_error(:birth_date_and_time, :equal_to)
end
it "should have error when value is equal to :equal_to restriction for all values except microscond, and microsecond is not ignored" do
configure_validator(:equal_to => Time.utc(2000, 1, 1, 0, 0, 0, 0), :ignore_usec => false)
validate_with(:birth_date_and_time, Time.utc(2000, 1, 1, 0, 0, 0, 500))
should_have_error(:birth_date_and_time, :equal_to)
end
it "should be valid when value is equal to :equal_to restriction for all values except microscond, and microsecond is ignored" do
configure_validator(:equal_to => Time.utc(2000, 1, 1, 0, 0, 0, 0), :ignore_usec => true)
validate_with(:birth_date_and_time, Time.utc(2000, 1, 1, 0, 0, 0, 500))
should_have_no_error(:birth_date_and_time, :equal_to)
end
it "should be valid when value is equal to :equal_to restriction" do
validate_with(:birth_date_and_time, Time.now)
should_have_no_error(:birth_date_and_time, :equal_to)
end
end
describe "for date type" do
before do
configure_validator(:type => :date, :equal_to => Date.today)
end
it "should have error when value is not equal to :equal_to restriction" do
validate_with(:birth_date, Date.today + 1)
should_have_error(:birth_date, :equal_to)
end
it "should be valid when value is equal to :equal_to restriction" do
validate_with(:birth_date, Date.today)
should_have_no_error(:birth_date, :equal_to)
end
end
describe "for time type" do
before do
configure_validator(:type => :time, :equal_to => "09:00:00")
end
it "should have error when value is not equal to :equal_to restriction" do
validate_with(:birth_time, "09:00:01")
should_have_error(:birth_time, :equal_to)
end
it "should be valid when value is equal to :equal_to restriction" do
validate_with(:birth_time, "09:00:00")
should_have_no_error(:birth_time, :equal_to)
end
end
end
describe "instance with :with_time option" do
it "should validate date attribute as datetime combining value of :with_time against restrictions " do
configure_validator(:type => :date, :with_time => '12:31', :on_or_before => Time.mktime(2000,1,1,12,30))
validate_with(:birth_date, "2000-01-01")
should_have_error(:birth_date, :on_or_before)
end
it "should skip restriction validation if :with_time value is nil" do
configure_validator(:type => :date, :with_time => nil, :on_or_before => Time.mktime(2000,1,1,12,30))
validate_with(:birth_date, "2000-01-01")
should_have_no_error(:birth_date, :on_or_before)
end
end
describe "instance with :with_date option" do
it "should validate time attribute as datetime combining value of :with_date against restrictions " do
configure_validator(:type => :time, :with_date => '2009-01-01', :on_or_before => Time.mktime(2000,1,1,12,30))
validate_with(:birth_date, "12:30")
should_have_error(:birth_date, :on_or_before)
end
it "should skip restriction validation if :with_date value is nil" do
configure_validator(:type => :time, :with_date => nil, :on_or_before => Time.mktime(2000,1,1,12,30))
validate_with(:birth_date, "12:30")
should_have_no_error(:birth_date, :on_or_before)
end
end
describe "instance with mixed value and restriction types" do
it "should validate datetime attribute with Date restriction" do
@@ -363,6 +473,20 @@ describe ValidatesTimeliness::Validator do
end
end
describe "instance with format option" do
it "should validate attribute when value matches format" do
configure_validator(:type => :time, :format => 'hh:nn:ss')
validate_with(:birth_time, "12:00:00")
should_have_no_error(:birth_time, :invalid_time)
end
it "should not validate attribute when value does not match format" do
configure_validator(:type => :time, :format => 'hh:nn:ss')
validate_with(:birth_time, "12:00")
should_have_error(:birth_time, :invalid_time)
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')
@@ -444,12 +568,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.error_value_formats
ValidatesTimeliness::Validator.error_value_formats = custom
end
end
it "should format datetime value of restriction" do
@@ -471,19 +601,27 @@ describe ValidatesTimeliness::Validator do
end
after :all do
ValidatesTimeliness::Validator.error_value_formats = @@formats
if defined?(I18n)
I18n.reload!
else
ValidatesTimeliness::Validator.error_value_formats = @@formats
end
end
end
end
def parse(*args)
ValidatesTimeliness::Parser.parse(*args)
end
def configure_validator(options={})
@validator = ValidatesTimeliness::Validator.new(options)
end
def validate_with(attr_name, value)
person.send("#{attr_name}=", value)
validator.call(person, attr_name)
validator.call(person, attr_name, value)
end
def should_have_error(attr_name, error)

View File

@@ -2,16 +2,16 @@
Gem::Specification.new do |s|
s.name = %q{validates_timeliness}
s.version = "1.1.2"
s.version = "2.0.0"
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-12}
s.date = %q{2009-04-12}
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"]
s.files = ["LICENSE", "README.rdoc", "Rakefile", "TODO", "CHANGELOG", "lib/validates_timeliness", "lib/validates_timeliness/core_ext", "lib/validates_timeliness/core_ext/date.rb", "lib/validates_timeliness/core_ext/date_time.rb", "lib/validates_timeliness/core_ext/time.rb", "lib/validates_timeliness/action_view", "lib/validates_timeliness/action_view/instance_tag.rb", "lib/validates_timeliness/locale", "lib/validates_timeliness/locale/en.yml", "lib/validates_timeliness/validation_methods.rb", "lib/validates_timeliness/active_record", "lib/validates_timeliness/active_record/attribute_methods.rb", "lib/validates_timeliness/active_record/multiparameter_attributes.rb", "lib/validates_timeliness/formats.rb", "lib/validates_timeliness/validator.rb", "lib/validates_timeliness/spec", "lib/validates_timeliness/spec/rails", "lib/validates_timeliness/spec/rails/matchers", "lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb", "lib/validates_timeliness.rb", "spec/core_ext", "spec/core_ext/dummy_time_spec.rb", "spec/validator_spec.rb", "spec/action_view", "spec/action_view/instance_tag_spec.rb", "spec/ginger_scenarios.rb", "spec/validation_methods_spec.rb", "spec/spec_helper.rb", "spec/formats_spec.rb", "spec/active_record", "spec/active_record/attribute_methods_spec.rb", "spec/active_record/multiparameter_attributes_spec.rb", "spec/time_travel", "spec/time_travel/time_travel.rb", "spec/time_travel/time_extensions.rb", "spec/time_travel/MIT-LICENSE", "spec/spec", "spec/spec/rails", "spec/spec/rails/matchers", "spec/spec/rails/matchers/validate_timeliness_spec.rb", "spec/resources", "spec/resources/person.rb", "spec/resources/sqlite_patch.rb", "spec/resources/schema.rb", "spec/resources/application.rb"]
s.files = ["LICENSE", "README.rdoc", "Rakefile", "TODO", "CHANGELOG", "lib/validates_timeliness", "lib/validates_timeliness/core_ext", "lib/validates_timeliness/core_ext/date.rb", "lib/validates_timeliness/core_ext/date_time.rb", "lib/validates_timeliness/core_ext/time.rb", "lib/validates_timeliness/action_view", "lib/validates_timeliness/action_view/instance_tag.rb", "lib/validates_timeliness/locale", "lib/validates_timeliness/locale/en.yml", "lib/validates_timeliness/validation_methods.rb", "lib/validates_timeliness/active_record", "lib/validates_timeliness/active_record/attribute_methods.rb", "lib/validates_timeliness/active_record/multiparameter_attributes.rb", "lib/validates_timeliness/parser.rb", "lib/validates_timeliness/formats.rb", "lib/validates_timeliness/validator.rb", "lib/validates_timeliness/spec", "lib/validates_timeliness/spec/rails", "lib/validates_timeliness/spec/rails/matchers", "lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb", "lib/validates_timeliness.rb", "spec/core_ext", "spec/core_ext/dummy_time_spec.rb", "spec/validator_spec.rb", "spec/action_view", "spec/action_view/instance_tag_spec.rb", "spec/ginger_scenarios.rb", "spec/spec_helper.rb", "spec/formats_spec.rb", "spec/active_record", "spec/active_record/attribute_methods_spec.rb", "spec/active_record/multiparameter_attributes_spec.rb", "spec/time_travel", "spec/time_travel/time_travel.rb", "spec/time_travel/time_extensions.rb", "spec/time_travel/MIT-LICENSE", "spec/parser_spec.rb", "spec/spec", "spec/spec/rails", "spec/spec/rails/matchers", "spec/spec/rails/matchers/validate_timeliness_spec.rb", "spec/resources", "spec/resources/person.rb", "spec/resources/sqlite_patch.rb", "spec/resources/schema.rb", "spec/resources/application.rb"]
s.has_rdoc = true
s.homepage = %q{http://github.com/adzap/validates_timeliness}
s.require_paths = ["lib"]