Compare commits

..

63 Commits
1.0.0 ... 1.1.6

Author SHA1 Message Date
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
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
20 changed files with 824 additions and 430 deletions

1
.gitignore vendored Normal file
View File

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

View File

@@ -1,3 +1,33 @@
= 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
- 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] = 1.0.0 [2008-12-06]
- Gemified! - Gemified!
- Refactor of plugin into a Data Mapper style validator class which makes for a cleaner implementation and possible future Merb\Data Mapper support - 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 = validates_timeliness
* Source: http://github.com/adzap/validates_timeliness * Source: http://github.com/adzap/validates_timeliness
* Bugs: http://adzap.lighthouseapp.com/projects/14111-validates_timeliness * Bugs: http://adzap.lighthouseapp.com/projects/14111-validates_timeliness
== DESCRIPTION: == DESCRIPTION:
@@ -24,17 +24,25 @@ think should be a valid date or time string.
* Respects new timezone features of Rails 2.1. * 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: == INSTALLATION:
As plugin (from master) As plugin (from master)
./script/plugin git://github.com/adzap/validates_timeliness ./script/plugin install git://github.com/adzap/validates_timeliness.git
As gem As gem
sudo gem install validates_timeliness sudo gem install validates_timeliness
# in environment.rb
config.gem 'validates_timeliness'
== USAGE: == USAGE:
@@ -47,47 +55,54 @@ validation method
end end
The list of validation methods available are as follows: The list of validation methods available are as follows:
validates_date - validate value as date
* 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_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 validation methods take the usual options plus some specific ones to restrict
the valid range of dates or times allowed the valid range of dates or times allowed
Temporal options (or restrictions): Temporal options (or restrictions):
:before - Attribute must be before this value to be valid :equal_to - Attribute must be equal to value to be valid
:on_or_before - Attribute must be equal to or before this value to be valid :before - Attribute must be before this value to be valid
:after - Attribute must be after this value to be valid :on_or_before - Attribute must be equal to or before this value to be valid
:on_or_after - Attribute must be equal to or after 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: Regular validation options:
:allow_nil - Allow a nil value to be valid :allow_nil - Allow a nil value to be valid
:allow_blank - Allows a nil or empty string value to be valid :allow_blank - Allows a nil or empty string value to be valid
:if - Execute validation when :if evaluates true :if - Execute validation when :if evaluates true
:unless - Execute validation when :unless evaluates false :unless - Execute validation when :unless evaluates false
Message options: - Use these to override the default error messages Special options:
:invalid_date_message :with_time - Validate a date attribute value combined with a time value against any temporal restrictions
:invalid_time_message :with_date - Validate a time attribute value combined with a date value against any temporal restrictions
:invalid_datetime_message :ignore_usec - Ignores microsecond value on datetime restrictions
:before_message
:on_or_before_message
:after_message
:on_or_after_message
The temporal restrictions can take 4 different value types: 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
* String value The temporal restrictions, with_date and with_time can take 4 different value types:
* Date, Time, or DateTime object value * String value
* Proc or lambda object * Date, Time, or DateTime object value
* A symbol matching the method name in the model * 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 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 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: == EXAMPLES:
@@ -101,6 +116,8 @@ values are compared as dates.
validates_datetime :appointment_date, :before => Proc.new { 1.week.from_now } 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: === DATE/TIME FORMATS:
@@ -113,44 +130,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 you want. No complex regular expressions or duck punching (monkey patching) the
plugin is needed. plugin is needed.
Time formats: ==== Time formats:
hh:nn:ss hh:nn:ss
hh-nn-ss hh-nn-ss
h:nn h:nn
h.nn h.nn
h nn h nn
h-nn h-nn
h:nn_ampm h:nn_ampm
h.nn_ampm h.nn_ampm
h nn_ampm h nn_ampm
h-nn_ampm h-nn_ampm
h_ampm h_ampm
NOTE: Any time format without a meridian token (the 'ampm' token) is considered NOTE: Any time format without a meridian token (the 'ampm' token) is considered in 24 hour time.
in 24 hour time.
Date formats: ==== Date formats:
yyyy/mm/dd yyyy/mm/dd
yyyy-mm-dd yyyy-mm-dd
yyyy.mm.dd yyyy.mm.dd
m/d/yy OR d/m/yy m/d/yy OR d/m/yy
m\d\yy OR d\m\yy m\d\yy OR d\m\yy
d-m-yy d-m-yy
d.m.yy d.m.yy
d mmm yy d mmm yy
NOTE: To use non-US date formats see US/EURO FORMATS section NOTE: To use non-US date formats see US/EURO FORMATS section
Datetime formats: ==== Datetime formats:
m/d/yy h:nn:ss OR d/m/yy hh:nn:ss 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 OR d/m/yy h:nn
m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm m/d/yy h:nn_ampm OR d/m/yy h:nn_ampm
yyyy-mm-dd hh:nn:ss yyyy-mm-dd hh:nn:ss
yyyy-mm-dd h:nn yyyy-mm-dd h:nn
ddd mmm d hh:nn:ss zo yyyy # Ruby time string ddd mmm d hh:nn:ss zo yyyy # Ruby time string
yyyy-mm-ddThh:nn:ss(?:Z|zo) # ISO 8601 yyyy-mm-ddThh:nn:ss(?:Z|zo) # ISO 8601
NOTE: To use non-US date formats see US/EURO FORMATS section NOTE: To use non-US date formats see US/EURO FORMATS section
Here is what each format token means: Here is what each format token means:
@@ -193,7 +209,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 when it does date interpretation, and is in keeping PoLS (principle of least
surprise). 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 initializer or environment.rb
ValidatesTimeliness::Formats.remove_us_formats ValidatesTimeliness::Formats.remove_us_formats
@@ -212,7 +228,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". 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 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! Now "10 o'clock" will be a valid value. So easy, no more whingeing!
@@ -227,7 +243,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 your new format higher precedence than an existing format, you can include the
before option like so 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 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. you adding a new one and deleting an old one to get it to work.
@@ -264,7 +280,8 @@ For Rails 2.0/2.1:
:before => "must be before %s", :before => "must be before %s",
:on_or_before => "must be on or before %s", :on_or_before => "must be on or before %s",
:after => "must be after %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. Where %s is the interpolation value for the restriction.
@@ -275,8 +292,9 @@ Rails 2.2+ using the I18n system to define new defaults:
activerecord: activerecord:
errors: errors:
messages: messages:
on_or_before: "must equal to or before {{restriction}}" on_or_before: "must be equal to or before {{restriction}}"
on_or_after: "must equal to or after {{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 The {{restriction}} signifies where the interpolation value for the restriction
will be inserted. will be inserted.

View File

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

7
TODO
View File

@@ -1,8 +1,3 @@
- :between option
- :format 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 - valid formats could come from locale file
- add replace_formats instead add_formats :before

View File

@@ -21,14 +21,20 @@ module ValidatesTimeliness
class << self class << self
def load_error_messages_with_i18n def enable_datetime_select_extension!
I18n.load_path += [ LOCALE_PATH ] enable_datetime_select_invalid_value_extension!
enable_multiparameter_attributes_extension!
end end
def load_error_messages_without_i18n def load_error_messages
messages = YAML::load(IO.read(LOCALE_PATH)) if defined?(I18n)
errors = messages['en']['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h } I18n.load_path += [ LOCALE_PATH ]
::ActiveRecord::Errors.default_error_messages.update(errors) I18n.reload!
else
messages = YAML::load(IO.read(LOCALE_PATH))
errors = messages['en']['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h }
::ActiveRecord::Errors.default_error_messages.update(errors)
end
end end
def default_error_messages def default_error_messages
@@ -39,28 +45,13 @@ module ValidatesTimeliness
end end
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 def setup_for_rails
major, minor = Rails::VERSION::MAJOR, Rails::VERSION::MINOR major, minor = Rails::VERSION::MAJOR, Rails::VERSION::MINOR
self.send("setup_for_rails_#{major}_#{minor}")
self.default_timezone = ::ActiveRecord::Base.default_timezone self.default_timezone = ::ActiveRecord::Base.default_timezone
rescue self.enable_datetime_select_extension!
raise "Rails version #{Rails::VERSION::STRING} not yet supported by validates_timeliness plugin" self.load_error_messages
end end
end end
end end
ValidatesTimeliness.setup_for_rails ValidatesTimeliness.setup_for_rails
ValidatesTimeliness::Formats.compile_format_expressions

View File

@@ -1,4 +1,9 @@
module ValidatesTimeliness module ValidatesTimeliness
def self.enable_datetime_select_invalid_value_extension!
::ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::ActionView::InstanceTag)
end
module ActionView module ActionView
# Intercepts the date and time select helpers to allow the # Intercepts the date and time select helpers to allow the
@@ -41,5 +46,3 @@ module ValidatesTimeliness
end end
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 # 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 # 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. # 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 module AttributeMethods
def self.included(base) def self.included(base)
base.extend ClassMethods base.extend ClassMethods
base.class_eval do
if Rails::VERSION::STRING < '2.1' alias_method_chain :read_attribute, :timeliness
base.class_eval do class << self
class << self alias_method_chain :define_attribute_methods, :timeliness
def create_time_zone_conversion_attribute?(name, column)
false
end
end
end end
end end
end end
@@ -38,110 +30,95 @@ module ValidatesTimeliness
# and value can be used from cache. This prevents the raw date/time value from # 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 # being type cast using default Rails type casting when writing values
# to the database. # to the database.
def read_attribute(attr_name) def read_attribute_with_timeliness(attr_name)
attr_name = attr_name.to_s attr_name = attr_name.to_s
if !(value = @attributes[attr_name]).nil? if !(value = @attributes[attr_name]).nil?
if column = column_for_attribute(attr_name) column = column_for_attribute(attr_name)
if unserializable_attribute?(attr_name, column) if column && [:date, :time, :datetime].include?(column.type) && @attributes_cache.has_key?(attr_name)
unserialize_attribute(attr_name) return @attributes_cache[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
end end
else
nil
end end
read_attribute_without_timeliness(attr_name)
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 # If Rails dirty attributes is enabled then the value is added to
# changed attributes if changed. Can't use the default dirty checking # changed attributes if changed. Can't use the default dirty checking
# implementation as it chains the write_attribute method which deletes # implementation as it chains the write_attribute method which deletes
# the attribute from the cache. # the attribute from the cache.
def write_date_time_attribute(attr_name, value) def write_date_time_attribute(attr_name, value, type, time_zone_aware)
column = column_for_attribute(attr_name) new = self.class.parse_date_time(value, type)
old = read_attribute(attr_name) if defined?(::ActiveRecord::Dirty)
new = self.class.parse_date_time(value, column.type)
unless column.type == :date || new.nil? if new && type != :date
new = new.to_time rescue new new = new.to_time
new = new.in_time_zone if time_zone_aware
end end
if self.class.send(:create_time_zone_conversion_attribute?, attr_name, column) if defined?(::ActiveRecord::Dirty) && !changed_attributes.include?(attr_name)
new = new.in_time_zone rescue nil old = read_attribute(attr_name)
if old != new
changed_attributes[attr_name] = (old.duplicable? ? old.clone : old)
end
end end
@attributes_cache[attr_name] = new @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 @attributes[attr_name] = value
end 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(time, 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 module ClassMethods
# Override AR method to define attribute reader and writer method for # Define attribute reader and writer method for date, time and
# date, time and datetime attributes to use plugin parser. # datetime attributes to use plugin parser.
def define_attribute_methods def define_attribute_methods_with_timeliness
return if generated_methods? return if generated_methods?
columns_hash.each do |name, column| columns_hash.each do |name, column|
unless instance_method_already_implemented?(name) unless instance_method_already_implemented?(name)
if self.serialized_attributes[name] if [:date, :time, :datetime].include?(column.type)
define_read_method_for_serialized_attribute(name) time_zone_aware = create_time_zone_conversion_attribute?(name, column) rescue false
elsif create_time_zone_conversion_attribute?(name, column) define_read_method_for_dates_and_times(name, column.type, time_zone_aware)
define_read_method_for_time_zone_conversion(name)
else
define_read_method(name.to_sym, name, column)
end end
end end
unless instance_method_already_implemented?("#{name}=") unless instance_method_already_implemented?("#{name}=")
if [:date, :time, :datetime].include?(column.type) 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
else define_write_method_for_dates_and_times(name, column.type, time_zone_aware)
define_write_method(name.to_sym)
end end
end end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end end
define_attribute_methods_without_timeliness
end end
# Define write method for date, time and datetime columns # 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 method_body = <<-EOV
def #{attr_name}=(value) def #{attr_name}=(value)
write_date_time_attribute('#{attr_name}', value) write_date_time_attribute('#{attr_name}', value, #{type.inspect}, #{time_zone_aware})
end end
EOV EOV
evaluate_attribute_method attr_name, method_body, "#{attr_name}=" evaluate_attribute_method attr_name, method_body, "#{attr_name}="
end end
# Define time attribute reader. If reloading then check if cached, def define_read_method_for_dates_and_times(attr_name, type, time_zone_aware)
# which means its in local time. If local, convert with parser as local
# timezone, otherwise use read_attribute method for quick default type
# cast of values from database using default timezone.
def define_read_method_for_time_zone_conversion(attr_name)
method_body = <<-EOV method_body = <<-EOV
def #{attr_name}(reload = false) def #{attr_name}(reload = false)
cached = @attributes_cache['#{attr_name}'] read_date_time_attribute('#{attr_name}', #{type.inspect}, #{time_zone_aware}, reload)
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
end end
EOV EOV
evaluate_attribute_method attr_name, method_body evaluate_attribute_method attr_name, method_body

View File

@@ -1,6 +1,10 @@
module ValidatesTimeliness module ValidatesTimeliness
module ActiveRecord
def self.enable_multiparameter_attributes_extension!
::ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes)
end
module ActiveRecord
module MultiparameterAttributes module MultiparameterAttributes
def self.included(base) def self.included(base)
@@ -38,13 +42,13 @@ module ValidatesTimeliness
values = values.map(&:to_s) values = values.map(&:to_s)
case type case type
when :date when :date
extract_date_from_multiparameter_attributes(values) extract_date_from_multiparameter_attributes(values)
when :time when :time
extract_time_from_multiparameter_attributes(values) extract_time_from_multiparameter_attributes(values)
when :datetime when :datetime
date_values, time_values = values.slice!(0, 3), values date_values, time_values = values.slice!(0, 3), values
extract_date_from_multiparameter_attributes(date_values) + " " + extract_time_from_multiparameter_attributes(time_values) extract_date_from_multiparameter_attributes(date_values) + " " + extract_time_from_multiparameter_attributes(time_values)
end end
end end
@@ -60,5 +64,3 @@ module ValidatesTimeliness
end end
end end
ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes)

View File

@@ -1,3 +1,5 @@
require 'date'
module ValidatesTimeliness module ValidatesTimeliness
# A date and time format regular expression generator. Allows you to # A date and time format regular expression generator. Allows you to
@@ -10,16 +12,14 @@ module ValidatesTimeliness
# string values. # string values.
# #
class Formats class Formats
cattr_accessor :time_formats cattr_accessor :time_formats,
cattr_accessor :date_formats :date_formats,
cattr_accessor :datetime_formats :datetime_formats,
:time_expressions,
cattr_accessor :time_expressions :date_expressions,
cattr_accessor :date_expressions :datetime_expressions,
cattr_accessor :datetime_expressions :format_tokens,
:format_proc_args
cattr_accessor :format_tokens
cattr_accessor :format_proc_args
# Format tokens: # Format tokens:
# y = year # y = year
@@ -115,7 +115,7 @@ module ValidatesTimeliness
{ 'mm' => [ /m{2}/, '(\d{2})', :month ] }, { 'mm' => [ /m{2}/, '(\d{2})', :month ] },
{ 'm' => [ /(\A|[^ap])m{1}/, '(\d{1,2})', :month ] }, { 'm' => [ /(\A|[^ap])m{1}/, '(\d{1,2})', :month ] },
{ 'yyyy' => [ /y{4,}/, '(\d{4})', :year ] }, { '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 ] }, { 'hh' => [ /h{2,}/, '(\d{2})', :hour ] },
{ 'h' => [ /h{1}/, '(\d{1,2})', :hour ] }, { 'h' => [ /h{1}/, '(\d{1,2})', :hour ] },
{ 'nn' => [ /n{2,}/, '(\d{2})', :min ] }, { 'nn' => [ /n{2,}/, '(\d{2})', :min ] },
@@ -139,13 +139,13 @@ module ValidatesTimeliness
# should just be the arg name. # should just be the arg name.
# #
@@format_proc_args = { @@format_proc_args = {
:year => [0, 'y', 'unambiguous_year(y)'], :year => [0, 'y', 'unambiguous_year(y)'],
:month => [1, 'm', 'month_index(m)'], :month => [1, 'm', 'month_index(m)'],
:day => [2, 'd', 'd'], :day => [2, 'd', 'd'],
:hour => [3, 'h', 'full_hour(h,md)'], :hour => [3, 'h', 'full_hour(h,md)'],
:min => [4, 'n', 'n'], :min => [4, 'n', 'n'],
:sec => [5, 's', 's'], :sec => [5, 's', 's'],
:usec => [6, 'u', 'microseconds(u)'], :usec => [6, 'u', 'microseconds(u)'],
:meridian => [nil, 'md', nil] :meridian => [nil, 'md', nil]
} }
@@ -164,16 +164,17 @@ module ValidatesTimeliness
def parse(string, type, strict=true) def parse(string, type, strict=true)
return string unless string.is_a?(String) return string unless string.is_a?(String)
expressions = expression_set(type, string) matches = nil
time_array = nil exp, processor = expression_set(type, string).find do |regexp, proc|
expressions.each do |(regexp, processor)| full = /\A#{regexp}\Z/ if strict
regexp = strict || type == :datetime ? /\A#{regexp}\Z/ : (type == :date ? /\A#{regexp}/ : /#{regexp}\Z/) full ||= case type
if matches = regexp.match(string.strip) when :date then /\A#{regexp}/
time_array = processor.call(*matches[1..7]) when :time then /#{regexp}\Z/
break when :datetime then /\A#{regexp}\Z/
end end
matches = full.match(string.strip)
end end
return time_array processor.call(*matches[1..7]) if matches
end end
# Delete formats of specified type. Error raised if format not found. # Delete formats of specified type. Error raised if format not found.
@@ -223,7 +224,7 @@ module ValidatesTimeliness
def format_expression_generator(string_format) def format_expression_generator(string_format)
regexp = string_format.dup regexp = string_format.dup
order = {} order = {}
regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes ]/ regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
format_tokens.each do |token| format_tokens.each do |token|
token_name = token.keys.first token_name = token.keys.first
@@ -237,8 +238,7 @@ module ValidatesTimeliness
return Regexp.new(regexp), format_proc(order) return Regexp.new(regexp), format_proc(order)
rescue rescue
puts "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}." raise "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}."
raise
end end
# Generates a proc which when executed maps the regexp capture groups to a # Generates a proc which when executed maps the regexp capture groups to a
@@ -261,24 +261,24 @@ module ValidatesTimeliness
end end
def compile_formats(formats) 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 end
# Pick expression set and combine date and datetimes for # Pick expression set and combine date and datetimes for
# datetime attributes to allow date string as datetime # datetime attributes to allow date string as datetime
def expression_set(type, string) def expression_set(type, string)
case type case type
when :date when :date
date_expressions date_expressions
when :time when :time
time_expressions time_expressions
when :datetime when :datetime
# gives a speed-up for date string as datetime attributes # gives a speed-up for date string as datetime attributes
if string.length < 11 if string.length < 11
date_expressions + datetime_expressions date_expressions + datetime_expressions
else else
datetime_expressions + date_expressions datetime_expressions + date_expressions
end end
end end
end end
@@ -299,7 +299,15 @@ module ValidatesTimeliness
def month_index(month) def month_index(month)
return month.to_i if month.to_i.nonzero? 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 end
def microseconds(usec) def microseconds(usec)
@@ -308,3 +316,5 @@ module ValidatesTimeliness
end end
end end
end end
ValidatesTimeliness::Formats.compile_format_expressions

View File

@@ -5,7 +5,9 @@ en:
invalid_date: "is not a valid date" invalid_date: "is not a valid date"
invalid_time: "is not a valid time" invalid_time: "is not a valid time"
invalid_datetime: "is not a valid datetime" invalid_datetime: "is not a valid datetime"
equal_to: "must be equal to {{restriction}}"
before: "must be before {{restriction}}" before: "must be before {{restriction}}"
on_or_before: "must be on or before {{restriction}}" on_or_before: "must be on or before {{restriction}}"
after: "must be after {{restriction}}" after: "must be after {{restriction}}"
on_or_after: "must be on or 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 Rails
module Matchers module Matchers
class ValidateTimeliness class ValidateTimeliness
cattr_accessor :test_values
@@test_values = { VALIDITY_TEST_VALUES = {
:date => {:pass => '2000-01-01', :fail => '2000-01-32'}, :date => {:pass => '2000-01-01', :fail => '2000-01-32'},
:time => {:pass => '12:00', :fail => '25:00'}, :time => {:pass => '12:00', :fail => '25:00'},
:datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00: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) def initialize(attribute, options)
@expected, @options = attribute, options @expected, @options = attribute, options
@validator = ValidatesTimeliness::Validator.new(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 end
def matches?(record) def matches?(record)
@record = record @record = record
type = options[:type] @type = @options[:type]
invalid_value = @@test_values[type][:fail] valid = test_validity
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_option(:before, :-) if options[:before] && valid valid = test_option(:before) if @options[:before] && valid
valid = test_option(:after, :+) if options[:after] && 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_before) 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_after) if @options[:on_or_after] && valid
valid = test_between if @options[:between] && valid
return valid return valid
end end
def failure_message 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 end
def negative_failure_message def negative_failure_message
"expected not to validate #{options[:type]} attribute #{expected.inspect}" "expected not to validate #{@type} attribute #{@expected.inspect}"
end end
def description def description
"have validated #{options[:type]} attribute #{expected.inspect}" "have validated #{@type} attribute #{@expected.inspect}"
end end
private private
attr_reader :actual, :expected, :record, :options, :messages, :last_failure, :validator
def test_option(option, modifier, settings={}) def test_validity
settings.reverse_merge!(:modify_on => :valid) invalid_value = VALIDITY_TEST_VALUES[@type][:fail]
boundary = parse_and_cast(options[option]) 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
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 valid_value, invalid_value = if settings[:modify_on] == :valid
[ boundary.send(modifier, 1), boundary ] [ boundary.send(method, 1), boundary ]
else else
[ boundary, boundary.send(modifier, 1) ] [ boundary, boundary.send(method, 1) ]
end end
message = messages[option] error_matching(invalid_value, option) &&
error_matching(invalid_value, /#{message}/) && no_error_matching(valid_value, option)
no_error_matching(valid_value, /#{message}/) 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 end
def parse_and_cast(value) def parse_and_cast(value)
value = validator.send(:restriction_value, value, record) value = @validator.class.send(:evaluate_option_value, value, @type, @record)
validator.send(:type_cast_value, value) @validator.class.send(:type_cast_value, value, @type)
end end
def error_matching(value, match) def error_matching(value, option)
record.send("#{expected}=", value) match = error_message_for(option)
record.valid? @record.send("#{@expected}=", value)
errors = record.errors.on(expected) @record.valid?
pass = [ errors ].flatten.any? {|error| match === error } errors = @record.errors.on(@expected)
@last_failure = "error matching #{match.inspect} when value is #{format_value(value)}" unless pass pass = [ errors ].flatten.any? {|error| /#{match}/ === error }
@last_failure = "error matching '#{match}' when value is #{format_value(value)}" unless pass
pass pass
end end
def no_error_matching(value, match) def no_error_matching(value, option)
pass = !error_matching(value, match) pass = !error_matching(value, option)
@last_failure = "no error matching #{match.inspect} when value is #{format_value(value)}" unless pass unless pass
error = error_message_for(option)
@last_failure = "no error matching '#{error}' when value is #{format_value(value)}"
end
pass pass
end end
def error_message_for(option)
msg = @validator.send(: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.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)
msg = @record.errors.generate_message(@expected, option, interpolate)
else
msg = msg % interpolate
end
end
msg
end
def format_value(value) def format_value(value)
return value if value.is_a?(String) 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
end end
def validate_date(attribute, options={}) def validate_date(attribute, options={})
options[:type] = :date options[:type] = :date
validate_timeliness_of(attribute, options) ValidateTimeliness.new(attribute, options)
end end
def validate_time(attribute, options={}) def validate_time(attribute, options={})
options[:type] = :time options[:type] = :time
validate_timeliness_of(attribute, options) ValidateTimeliness.new(attribute, options)
end end
def validate_datetime(attribute, options={}) def validate_datetime(attribute, options={})
options[:type] = :datetime options[:type] = :datetime
validate_timeliness_of(attribute, options)
end
private
def validate_timeliness_of(attribute, options={})
ValidateTimeliness.new(attribute, options) ValidateTimeliness.new(attribute, options)
end end
end end
end end
end end

View File

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

View File

@@ -11,18 +11,33 @@ module ValidatesTimeliness
:datetime => '%Y-%m-%d %H:%M:%S' :datetime => '%Y-%m-%d %H:%M:%S'
} }
RESTRICTION_METHODS = {
:equal_to => :==,
:before => :<,
:after => :>,
:on_or_before => :<=,
:on_or_after => :>=,
:between => lambda {|v, r| (r.first..r.last).include?(v) }
}
VALID_OPTIONS = [
:on, :if, :unless, :allow_nil, :empty, :allow_blank, :blank,
:with_time, :with_date, :ignore_usec,
:invalid_time_message, :invalid_date_message, :invalid_datetime_message
] + RESTRICTION_METHODS.keys.map {|option| [option, "#{option}_message".to_sym] }.flatten
attr_reader :configuration, :type attr_reader :configuration, :type
def initialize(configuration) 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) @configuration = defaults.merge(configuration)
@type = @configuration.delete(:type) @type = @configuration.delete(:type)
validate_options(@configuration)
end end
def call(record, attr_name) def call(record, attr_name, value)
value = record.send(attr_name)
value = record.class.parse_date_time(value, type, false) if value.is_a?(String) value = record.class.parse_date_time(value, type, false) if value.is_a?(String)
raw_value = raw_value(record, attr_name) raw_value = raw_value(record, attr_name) || value
return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank]) return if (raw_value.nil? && configuration[:allow_nil]) || (raw_value.blank? && configuration[:allow_blank])
@@ -36,25 +51,28 @@ module ValidatesTimeliness
private private
def raw_value(record, attr_name) def raw_value(record, attr_name)
record.send("#{attr_name}_before_type_cast") record.send("#{attr_name}_before_type_cast") rescue nil
end end
def validate_restrictions(record, attr_name, value) def validate_restrictions(record, attr_name, value)
restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='} 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?
display = self.class.error_value_formats[type] RESTRICTION_METHODS.each do |option, method|
value = type_cast_value(value)
restriction_methods.each do |option, method|
next unless restriction = configuration[option] next unless restriction = configuration[option]
begin begin
compare = restriction_value(restriction, record) restriction = self.class.evaluate_option_value(restriction, restriction_type, record)
next if compare.nil? next if restriction.nil?
compare = type_cast_value(compare) restriction = self.class.type_cast_value(restriction, restriction_type, @configuration[:ignore_usec])
unless value.send(method, compare) unless evaluate_restriction(restriction, value, method)
add_error(record, attr_name, option, :restriction => compare.strftime(display)) add_error(record, attr_name, option, interpolation_values(option, restriction))
end end
rescue rescue
unless self.class.ignore_restriction_errors unless self.class.ignore_restriction_errors
@@ -64,56 +82,123 @@ module ValidatesTimeliness
end end
end end
def add_error(record, attr_name, message, interpolate={}) def interpolation_values(option, restriction)
if Rails::VERSION::STRING < '2.2' format = self.class.error_value_formats[type]
message = error_messages[message] if message.is_a?(Symbol) restriction = [restriction] unless restriction.is_a?(Array)
message = message % interpolate.values unless interpolate.empty?
record.errors.add(attr_name, message) 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 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 # use i18n support in AR for message or use custom message passed to validation method
custom = custom_error_messages[message] 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
end end
def error_messages def error_messages
return @error_messages if defined?(@error_messages) @error_messages ||= ValidatesTimeliness.default_error_messages.merge(custom_error_messages)
@error_messages = ValidatesTimeliness.default_error_messages.merge(custom_error_messages)
end end
def custom_error_messages 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({}) {|h, (k, v)| h[$1.to_sym] = v if k.to_s =~ /(.*)_message$/;h } if md = /(.*)_message$/.match(k.to_s)
msgs[md[1].to_sym] = v
end
msgs
}
end end
def restriction_value(restriction, record) def combine_date_and_time(value, record)
case restriction 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?
record.class.send(:make_time, [date.year, date.month, date.day, time.hour, time.min, time.sec, time.usec])
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 evaluate_option_value(value, type, record)
case value
when Time, Date, DateTime when Time, Date, DateTime
restriction value
when Symbol when Symbol
restriction_value(record.send(restriction), record) evaluate_option_value(record.send(value), type, record)
when Proc when Proc
restriction_value(restriction.call(record), record) 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 else
record.class.parse_date_time(restriction, type, false) record.class.parse_date_time(value, type, false)
end
end end
end
def type_cast_value(value) def type_cast_value(value, type, ignore_usec=false)
case type if value.is_a?(Array)
when :time value.map {|v| type_cast_value(v, type, ignore_usec) }
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(ValidatesTimelines.default_timezone)
end
else else
nil 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)
::ActiveRecord::Base.send(:make_time, Array(value).reverse[4..9])
else
value
end
end
end end
end end
end end

View File

@@ -1,9 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ValidatesTimeliness::ActiveRecord::AttributeMethods do describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
include ValidatesTimeliness::ActiveRecord::AttributeMethods
include ValidatesTimeliness::ValidationMethods
before do before do
@person = Person.new @person = Person.new
end end
@@ -23,6 +20,24 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date_and_time = "2000-01-01 12:00" @person.birth_date_and_time = "2000-01-01 12:00"
end 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 it "should call parser on write for datetime attribute" do
@person.class.should_receive(:parse_date_time).once @person.class.should_receive(:parse_date_time).once
@person.birth_date_and_time = "2000-01-01 02:03:04" @person.birth_date_and_time = "2000-01-01 02:03:04"
@@ -54,6 +69,11 @@ describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
@person.birth_date_and_time.should be_kind_of(Time) @person.birth_date_and_time.should be_kind_of(Time)
end 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 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 = "2000-01-01 02:03:04"
@person.birth_date_and_time.should be_kind_of(Time) @person.birth_date_and_time.should be_kind_of(Time)

View File

@@ -163,6 +163,11 @@ describe ValidatesTimeliness::Formats do
time_array.should == [2000,2,1,0,0,0,0] time_array.should == [2000,2,1,0,0,0,0]
end 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 it "should ignore date when extracting time and strict is false" do
time_array = formats.parse('2000-02-01 12:12', :time, false) time_array = formats.parse('2000-02-01 12:12', :time, false)
time_array.should == [0,0,0,12,12,0,0] time_array.should == [0,0,0,12,12,0,0]

View File

@@ -9,7 +9,7 @@
# ginger spec # ginger spec
# #
Ginger.configure do |config| 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| rails_versions.each do |v|
g = Ginger::Scenario.new g = Ginger::Scenario.new

View File

@@ -5,42 +5,59 @@ end
class WithValidation < Person class WithValidation < Person
validates_date :birth_date, validates_date :birth_date,
:before => '2000-01-10', :after => '2000-01-01', :before => '2000-01-10',
:on_or_before => '2000-01-09', :on_or_after => '2000-01-02' :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, validates_time :birth_time,
:before => '23:00', :after => '09:00', :before => '23:00',
:on_or_before => '22:00', :on_or_after => '10: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, validates_datetime :birth_date_and_time,
:before => '2000-01-10 23:00', :after => '2000-01-01 09:00', :before => '2000-01-10 23:00',
:on_or_before => '2000-01-09 23:00', :on_or_after => '2000-01-02 09: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 end
class CustomMessages < Person class CustomMessages < Person
validates_date :birth_date, :invalid_date_message => 'is not really a date', validates_date :birth_date,
:before => '2000-01-10', :before_message => 'is too late', :invalid_date_message => 'is not really a date',
:after => '2000-01-01', :after_message => 'is too early', :before => '2000-01-10',
:on_or_before=> '2000-01-09', :on_or_before_message => 'is just too late', :before_message => 'is too late',
:on_or_after => '2000-01-02', :on_or_after_message => 'is just too early' :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 end
describe "ValidateTimeliness matcher" do describe "ValidateTimeliness matcher" do
attr_accessor :no_validation, :with_validation attr_accessor :no_validation, :with_validation
@@attribute_for_type = { :date => :birth_date, :time => :birth_time, :datetime => :birth_date_and_time }
before do before do
@no_validation = NoValidation.new @no_validation = NoValidation.new
@with_validation = WithValidation.new @with_validation = WithValidation.new
end end
[:date, :time, :datetime].each do |type| [:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do 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 end
it "should report that #{type} is not validated" do 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
end end
@@ -52,18 +69,17 @@ describe "ValidateTimeliness matcher" do
} }
[:date, :time, :datetime].each do |type| [:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do 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 end
it "should report that #{type} is not validated when option value is incorrect" do 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 end
it "should report that #{type} is not validated with option" do 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 end
end end
@@ -76,18 +92,17 @@ describe "ValidateTimeliness matcher" do
} }
[:date, :time, :datetime].each do |type| [:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do 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 end
it "should report that #{type} is not validated when option value is incorrect" do 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 end
it "should report that #{type} is not validated with option" do 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 end
end end
@@ -100,18 +115,17 @@ describe "ValidateTimeliness matcher" do
} }
[:date, :time, :datetime].each do |type| [:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do 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 end
it "should report that #{type} is not validated when option value is incorrect" do 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 end
it "should report that #{type} is not validated with option" do 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 end
end end
@@ -124,18 +138,40 @@ describe "ValidateTimeliness matcher" do
} }
[:date, :time, :datetime].each do |type| [:date, :time, :datetime].each do |type|
attribute = type == :datetime ? :date_and_time : type
it "should report that #{type} is validated" do 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 end
it "should report that #{type} is not validated when option value is incorrect" do 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 end
it "should report that #{type} is not validated with option" do 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 end
end end
@@ -175,4 +211,8 @@ describe "ValidateTimeliness matcher" do
end end
end end
def attribute_for_type(type)
@@attribute_for_type[type.to_sym]
end
end end

View File

@@ -16,36 +16,71 @@ describe ValidatesTimeliness::Validator do
@person = Person.new @person = Person.new
end 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 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 end
it "should return Time object when restriction is string" do 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 end
it "should return Time object when restriction is method and method returns Time object" do it "should return Time object when restriction is method and method returns Time object" do
person.stub!(:datetime_attr).and_return(Time.now) 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 end
it "should return Time object when restriction is method and method returns string" do 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") 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 end
it "should return Time object when restriction is proc which returns Time object" do 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 end
it "should return Time object when restriction is proc which returns string" do 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 end
def restriction_value(restriction, type) it "should return array of Time objects when restriction is array of Time objects" do
time1, time2 = Time.now, 1.day.ago
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"
evaluate_option_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
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"
evaluate_option_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
end
def evaluate_option_value(restriction, type)
configure_validator(:type => type) configure_validator(:type => type)
validator.send(:restriction_value, restriction, person) validator.class.send(:evaluate_option_value, restriction, type, person)
end end
end end
@@ -212,84 +247,196 @@ describe ValidatesTimeliness::Validator do
end end
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 describe "for datetime type" do
before 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 end
it "should have error when value is past :on_or_before restriction" do it "should have error when value is before earlist :between restriction" do
validate_with(:birth_date_and_time, Time.now.at_midnight + 1) validate_with(:birth_date_and_time, 2.days.ago)
should_have_error(:birth_date_and_time, :on_of_before) should_have_error(:birth_date_and_time, :between)
end end
it "should be valid when value is equal to :on_or_before restriction" do it "should have error when value is after latest :between restriction" do
validate_with(:birth_date_and_time, Time.now.at_midnight) validate_with(:birth_date_and_time, 2.days.from_now)
should_have_no_error(:birth_date_and_time, :on_of_before) should_have_error(:birth_date_and_time, :between)
end end
it "should have error when value is before :on_or_after restriction" do it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_date_and_time, 1.days.ago - 1) validate_with(:birth_date_and_time, 1.day.ago.at_midnight)
should_have_error(:birth_date_and_time, :on_of_after) should_have_no_error(:birth_date_and_time, :between)
end end
it "should be valid when value is value equal to :on_or_after restriction" do it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_date_and_time, 1.day.ago) validate_with(:birth_date_and_time, 1.day.from_now.at_midnight)
should_have_no_error(:birth_date_and_time, :on_of_after) 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
end end
describe "for date type" do describe "for date type" do
before :each do before do
configure_validator(:on_or_before => 1.day.from_now, :on_or_after => 1.day.ago, :type => :date) configure_validator(:type => :date, :between => [1.day.ago.to_date, 1.day.from_now.to_date])
end end
it "should have error when value is past :on_or_before restriction" do it "should have error when value is before earlist :between restriction" do
validate_with(:birth_date, 2.days.from_now) validate_with(:birth_date, 2.days.ago.to_date)
should_have_error(:birth_date, :on_or_before) should_have_error(:birth_date, :between)
end end
it "should have error when value is before :on_or_after restriction" do it "should have error when value is after latest :between restriction" do
validate_with(:birth_date, 2.days.ago) validate_with(:birth_date, 2.days.from_now.to_date)
should_have_error(:birth_date, :on_or_after) should_have_error(:birth_date, :between)
end end
it "should be valid when value is equal to :on_or_before restriction" do it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_date, 1.day.from_now) validate_with(:birth_date, 1.day.ago.to_date)
should_have_no_error(:birth_date, :on_or_before) should_have_no_error(:birth_date, :between)
end end
it "should be valid when value value is equal to :on_or_after restriction" do it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_date, 1.day.ago) validate_with(:birth_date, 1.day.from_now.to_date)
should_have_no_error(:birth_date, :on_or_before) 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
end end
describe "for time type" do describe "for time type" do
before :each do before do
configure_validator(:on_or_before => "23:00", :on_or_after => "06:00", :type => :time) configure_validator(:type => :time, :between => ["09:00", "17:00"])
end end
it "should have error when value is past :on_or_before restriction" do it "should have error when value is before earlist :between restriction" do
validate_with(:birth_time, "23:01") validate_with(:birth_time, "08:59")
should_have_error(:birth_time, :on_or_before) should_have_error(:birth_time, :between)
end end
it "should have error when value is before :on_or_after restriction" do it "should have error when value is after latest :between restriction" do
validate_with(:birth_time, "05:59") validate_with(:birth_time, "17:01")
should_have_error(:birth_time, :on_or_after) should_have_error(:birth_time, :between)
end end
it "should be valid when value is on boundary of :on_or_before restriction" do it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_time, "23:00") validate_with(:birth_time, "09:00")
should_have_no_error(:birth_time, :on_or_before) should_have_no_error(:birth_time, :between)
end end
it "should be valid when value is on boundary of :on_or_after restriction" do it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_time, "06:00") validate_with(:birth_time, "17:00")
should_have_no_error(:birth_time, :on_or_after) should_have_no_error(:birth_time, :between)
end 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
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
end end
@@ -326,6 +473,40 @@ describe ValidatesTimeliness::Validator do
end end
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 describe "restriction errors" do
before :each do before :each do
configure_validator(:type => :date, :before => lambda { raise }) configure_validator(:type => :date, :before => lambda { raise })
@@ -412,7 +593,7 @@ describe ValidatesTimeliness::Validator do
def validate_with(attr_name, value) def validate_with(attr_name, value)
person.send("#{attr_name}=", value) person.send("#{attr_name}=", value)
validator.call(person, attr_name) validator.call(person, attr_name, value)
end end
def should_have_error(attr_name, error) def should_have_error(attr_name, error)
@@ -433,6 +614,6 @@ describe ValidatesTimeliness::Validator do
def error_messages def error_messages
return @error_messages if defined?(@error_messages) return @error_messages if defined?(@error_messages)
messages = validator.send(: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
end end

View File

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