Compare commits

..

47 Commits
0.1.0 ... 1.1.0

Author SHA1 Message Date
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
Adam Meehan
897a9a3bd3 final touch ups to changelog, todo and gemspec for release 1.0.0 2008-12-07 12:06:42 +11:00
Adam Meehan
e9fa4ca20a fixed bug where accessor methods not properly generating due columns_hash lookup failing on method name as a symbol
force value to time on write unless is a date attribute
2008-12-07 11:07:39 +11:00
Adam Meehan
e4760126e2 allow for a string column being validated as a date/time 2008-12-07 11:06:43 +11:00
Adam Meehan
12af4d8d9d updated changelog and added to gem files 2008-12-06 15:43:56 +11:00
Adam Meehan
81330e7aad gemified using newgem with --simple option 2008-12-06 14:58:05 +11:00
Adam Meehan
e82b1e2033 removed some time_travel setup which are not used here 2008-12-06 14:42:21 +11:00
Adam Meehan
2ba85772b6 renamed license file 2008-12-06 14:42:02 +11:00
Adam Meehan
64ffb52dae spec text tweaks 2008-12-06 14:27:20 +11:00
Adam Meehan
ea6ec0cd75 added rdoc extension to readme for github prettiness 2008-12-06 14:25:58 +11:00
Adam Meehan
a691b4ed35 moved sqlite patch into seperate file 2008-12-06 09:06:24 +11:00
Adam Meehan
e2790538a8 added make_time examples 2008-12-05 20:46:40 +11:00
Adam Meehan
75f3ef34e8 made parameters explicit for validates_timeliness since its private and should not be used directly for validation 2008-12-05 20:35:37 +11:00
Adam Meehan
4cb51ae602 teeny comment correction 2008-12-05 20:30:54 +11:00
Adam Meehan
dad55456d5 patched sqlite adapter in spec_helper to fix time attributes in rails 2.0.2 errorneously reporting time attributes as datetime column types 2008-12-05 20:25:28 +11:00
Adam Meehan
87b0beef5a moved ignore_restriction_errors and error_value_formats into Validator class 2008-12-05 20:24:06 +11:00
Adam Meehan
6cd6cd9dc0 made restriction_value instance method as it relies on validator instance attribute type
removed old type_cast_method class method
2008-12-05 18:45:28 +11:00
Adam Meehan
0c5cc1a536 more refactoring of validator
fixed bug when Date is restriction value for a datetime attribute so its cast to time in default timezone
2008-12-05 18:25:44 +11:00
Adam Meehan
c224db7af8 moved specs to validator 2008-12-05 18:24:31 +11:00
Adam Meehan
4868746e94 refactor specs from validation_methods to validator and refactored to make much more sense 2008-12-05 18:23:39 +11:00
Adam Meehan
bf999170d7 fix timezone issue for to_dummy_time as per normal Rails which uses AR default_timezone 2008-12-05 16:34:28 +11:00
Adam Meehan
dbfd9231b5 moved including of modules into module files 2008-12-03 21:44:39 +11:00
Adam Meehan
3da24f0f33 a few changes to prepare for better version support 2008-12-03 21:22:19 +11:00
Adam Meehan
6f4306973b updated some comments 2008-12-03 21:20:53 +11:00
Adam Meehan
71f2a43424 added i18n support for error messages in Rails 2.2 2008-12-03 18:20:34 +11:00
Adam Meehan
c386a9cdcf removed some naff comments 2008-12-02 21:40:41 +11:00
Adam Meehan
1596ffd2cb namespaced matcher 2008-12-02 21:07:08 +11:00
Adam Meehan
d847d3b95a removed a debug hint in matcher 2008-12-02 20:42:00 +11:00
Adam Meehan
d0e60ece92 updated with new plugin settings method names and some refinements 2008-12-02 20:35:12 +11:00
Adam Meehan
3ac65b507f renamed methods which have been moved out of AR and so dont need to be fully qualified 2008-12-02 20:34:02 +11:00
Adam Meehan
d71f581e10 refactored error message handling and specs 2008-12-02 19:36:03 +11:00
Adam Meehan
aa42fb76b6 get the model to add errors to record returned from validation instead of validator 2008-11-30 08:34:17 +11:00
Adam Meehan
b0647d456e refactored validations in validation methods for model and validator class 2008-11-30 07:40:11 +11:00
Adam Meehan
9610d79d7d move core extension spec to namespace folder 2008-11-29 18:55:18 +11:00
Adam Meehan
412ff22dd9 namespaced ActiveRecord and ActionView specifc modules and specs with a mind to making the plugin framework agnostic in the future 2008-11-29 18:32:32 +11:00
36 changed files with 1621 additions and 1230 deletions

1
.gitignore vendored Normal file
View File

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

View File

@@ -1,28 +1,46 @@
[2008-11-13] = 1.1.0 [2009-01-01]
- allow uppercase meridian to be valid [reported by Alex (http://alex.digns.com/)] - Added between option
[2008-10-28] = 1.0.0 [2008-12-06]
- fixed bug when dirty attributes not reflecting change when attribute changed from time value to nil [reported by Brad (pvjq)] - Gemified!
- fixes for Rails 2.2 compatibility. Will refactor in to Rails version specific branches in the future. - Refactor of plugin into a Data Mapper style validator class which makes for a cleaner implementation and possible future Merb\Data Mapper support
- Added Rails 2.2 i18n support. Plugin error messages can specified in locale files. See README.
- ignore_datetime_restriction_errors setting has been moved from AR to ValidatesTimeliness::Validator.ignore_restriction_errors
- date_time_error_value_formats setting has been moved from AR to ValidatesTimeliness::Validator.error_value_formats
- Namespaced modules and specs
- Clean up of specs
- fixed a few bugs
- accessor methods not generating properly due method name stored as symbol in generated_attributes which fails on lookup
- force value assigned to time/datetime attributes to time objects
[2008-09-24] = 0.1.0 [2008-12-06]
- refactored attribute write method definitions - Tagged plugin as version 0.1.0
[2008-08-25] = 2008-11-13
- fixed bug for non-timezone write method not updating changed attributes hash [reported by Sylvestre Mergulhão] - allow uppercase meridian to be valid [reported by Alex (http://alex.digns.com/)]
[2008-08-22] = 2008-10-28
- fixed bug with attribute cache not clearing on write for date and time columns [reported by Sylvestre Mergulhão] - fixed bug when dirty attributes not reflecting change when attribute changed from time value to nil [reported by Brad (pvjq)]
- parse method returns Date object for date column assigned string as per normal Rails behaviour - fixes for Rails 2.2 compatibility. Will refactor in to Rails version specific branches in the future.
- parse method returns same object type when assigned Date or Time object as per normal Rails behaviour
[2008-08-07] = 2008-09-24
- modified matcher option value parsing to allow same value types as validation method - refactored attribute write method definitions
- fixed matcher message
[2008-08-02] = 2008-08-25
- refactored validation - fixed bug for non-timezone write method not updating changed attributes hash [reported by Sylvestre Mergulhão]
- refactored matcher
[2008-07-30] = 2008-08-22
- removed setting values to nil when validation fails to preserve before_type_cast value - fixed bug with attribute cache not clearing on write for date and time columns [reported by Sylvestre Mergulhão]
- parse method returns Date object for date column assigned string as per normal Rails behaviour
- parse method returns same object type when assigned Date or Time object as per normal Rails behaviour
= 2008-08-07
- modified matcher option value parsing to allow same value types as validation method
- fixed matcher message
= 2008-08-02
- refactored validation
- refactored matcher
= 2008-07-30
- removed setting values to nil when validation fails to preserve before_type_cast value

View File

@@ -1,7 +1,7 @@
= validates_timeliness = validates_timeliness
* Source: http://github.com/adzap/validates_timeliness * Source: http://github.com/adzap/validates_timeliness
* Tickets: http://adzap.lighthouseapp.com/projects/14111-validates_timeliness * Bugs: http://adzap.lighthouseapp.com/projects/14111-validates_timeliness
== DESCRIPTION: == DESCRIPTION:
@@ -24,12 +24,20 @@ 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
== INSTALLATION: == INSTALLATION:
./script/plugin git://github.com/adzap/validates_timeliness As plugin (from master)
./script/plugin git://github.com/adzap/validates_timeliness.git
As gem
sudo gem install validates_timeliness
== USAGE: == USAGE:
To validate a model with a date, time or datetime attribute you just use the To validate a model with a date, time or datetime attribute you just use the
@@ -51,11 +59,12 @@ The list of validation methods available are as follows:
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: Temporal options (or restrictions):
:before - Attribute must be before this 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 :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 :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 :on_or_after - Attribute must be equal to or after this value to be valid
:between - Attribute must be between the values to be valid
Regular validation options: Regular validation options:
:allow_nil - Allow a nil value to be valid :allow_nil - Allow a nil value to be valid
@@ -71,15 +80,17 @@ the valid range of dates or times allowed
:on_or_before_message :on_or_before_message
:after_message :after_message
:on_or_after_message :on_or_after_message
:between_message
The temporal options can take 4 different value types: The temporal restrictions can take 4 different value types:
* String value * String value
* Date, Time, or DateTime object value * Date, Time, or DateTime object value
* Proc or lambda object * Proc or lambda object
* A symbol matching the method name in the model * A symbol matching the method name in the model
* Between option takes an array of two values or a range
When an attribute value is compared to temporal options, 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.
@@ -227,23 +238,17 @@ 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.
=== TEMPORAL OPTION ERRORS: === TEMPORAL RESTRICTION ERRORS:
When using the validation temporal options there are times when the restriction When using the validation temporal restrictions there are times when the restriction
value itself may be invalid. Normally this will add an error to the model such as value itself may be invalid. Normally this will add an error to the model such as
'restriction :before value was invalid'. These can be annoying if you are using 'restriction :before value was invalid'. These can be annoying if you are using
procs or methods as restrictions and don't care if they don't evaluate properly procs or methods as restrictions and don't care if they don't evaluate properly
and you want the validation to complete. In these situations you turn them off. and you want the validation to complete. In these situations you turn them off.
To turn them off globally: To turn them off:
ActiveRecord::Base.ignore_datetime_restriction_errors = true ValidatesTimeliness::Validator.ignore_restriction_errors = true
To switch them on or off per model:
class Person < ActiveRecord::Base
self.ignore_datetime_restriction_errors = false
end
A word of warning though, as this may hide issues with the model and make those A word of warning though, as this may hide issues with the model and make those
corner cases a little harder to test. In general if you are using procs or corner cases a little harder to test. In general if you are using procs or
@@ -252,9 +257,11 @@ return nil in all other situations. Restrictions are skipped if they are nil.
=== OTHER CUSTOMISATION: === OTHER CUSTOMISATION:
The error messages for each temporal option can also be globally overridden by The error messages for each temporal restrictions can also be globally overridden by
updating the default AR error messages like so updating the default AR error messages like so
For Rails 2.0/2.1:
ActiveRecord::Errors.default_error_messages.update( ActiveRecord::Errors.default_error_messages.update(
: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",
@@ -262,13 +269,29 @@ updating the default AR error messages like so
: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"
) )
Or for something a little more specific you can override the format of the values Where %s is the interpolation value for the restriction.
inserted in the error messages for temporal options like so
ActiveRecord::Errors.date_time_error_value_formats.update( Rails 2.2+ using the I18n system to define new defaults:
en:
activerecord:
errors:
messages:
on_or_before: "must be equal to or before {{restriction}}"
on_or_after: "must be equal to or after {{restriction}}"
between: "must be between {{earliest}} and {{latest}}"
The {{restriction}} signifies where the interpolation value for the restriction
will be inserted.
And for something a little more specific you can override the format of the interpolation
values inserted in the error messages for temporal restrictions like so
ValidatesTimeliness::Validator.error_value_formats.update(
:time => '%H:%M:%S', :time => '%H:%M:%S',
:date => '%Y-%m-%d', :date => '%Y-%m-%d',
:datetime => '%Y-%m-%d %H:%M:%S' :datetime => '%Y-%m-%d %H:%M:%S'
@@ -285,7 +308,6 @@ with the plugin or better yet *before* you write them! You just use the
validation options you want as you would with the validation method. Those validation options you want as you would with the validation method. Those
options are then verified and reported if they fail. Use it like so: options are then verified and reported if they fail. Use it like so:
@person.should validate_date(:birth_date, :before => Time.now, :before_message => 'should be before today') @person.should validate_date(:birth_date, :before => Time.now, :before_message => 'should be before today')

View File

@@ -1,14 +1,58 @@
require 'rake' require 'rubygems'
require 'rake/testtask' require 'rake/gempackagetask'
require 'rake/rdoctask' require 'rubygems/specification'
require 'date'
require 'spec/rake/spectask' require 'spec/rake/spectask'
spec_files = Rake::FileList["spec/**/*_spec.rb"] GEM = "validates_timeliness"
GEM_VERSION = "1.1.0"
AUTHOR = "Adam Meehan"
EMAIL = "adam.meehan@gmail.com"
HOMEPAGE = "http://github.com/adzap/validates_timeliness"
SUMMARY = "Date and time validation plugin for Rails 2.x which allows custom formats"
desc "Run specs" spec = Gem::Specification.new do |s|
Spec::Rake::SpecTask.new do |t| s.name = GEM
t.spec_files = spec_files s.version = GEM_VERSION
t.spec_opts = ["-c"] s.platform = Gem::Platform::RUBY
s.rubyforge_project = "validatestime"
s.has_rdoc = true
s.extra_rdoc_files = ["README.rdoc", "LICENSE", "TODO", "CHANGELOG"]
s.summary = SUMMARY
s.description = s.summary
s.author = AUTHOR
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}/**/*")
end end
task :default => :spec task :default => :spec
desc "Run specs"
Spec::Rake::SpecTask.new do |t|
t.spec_files = FileList['spec/**/*_spec.rb']
t.spec_opts = %w(-fs --color)
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.gem_spec = spec
end
desc "install the gem locally"
task :install => [:package] do
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
end
desc "create a gemspec file"
task :make_spec do
File.open("#{GEM}.gemspec", "w") do |file|
file.puts spec.to_ruby
end
end

8
TODO Normal file
View File

@@ -0,0 +1,8 @@
- :between option
- :format option
- :with_date and :with_time options
- Merb and Data Mapper support
- does it have before_type_cast
- timezone handling
- view helper support
- valid formats could come from locale file

View File

@@ -1,21 +1,66 @@
require 'validates_timeliness/attribute_methods'
require 'validates_timeliness/validations'
require 'validates_timeliness/formats' require 'validates_timeliness/formats'
require 'validates_timeliness/multiparameter_attributes' require 'validates_timeliness/validator'
require 'validates_timeliness/instance_tag' require 'validates_timeliness/validation_methods'
require 'validates_timeliness/validate_timeliness_matcher' if ENV['RAILS_ENV'] == 'test' require 'validates_timeliness/spec/rails/matchers/validate_timeliness' if ENV['RAILS_ENV'] == 'test'
require 'validates_timeliness/active_record/attribute_methods'
require 'validates_timeliness/active_record/multiparameter_attributes'
require 'validates_timeliness/action_view/instance_tag'
require 'validates_timeliness/core_ext/time' require 'validates_timeliness/core_ext/time'
require 'validates_timeliness/core_ext/date' require 'validates_timeliness/core_ext/date'
require 'validates_timeliness/core_ext/date_time' require 'validates_timeliness/core_ext/date_time'
ActiveRecord::Base.send(:include, ValidatesTimeliness::AttributeMethods) module ValidatesTimeliness
ActiveRecord::Base.send(:include, ValidatesTimeliness::Validations)
ActiveRecord::Base.send(:include, ValidatesTimeliness::MultiparameterAttributes) mattr_accessor :default_timezone
ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::InstanceTag)
Time.send(:include, ValidatesTimeliness::CoreExtensions::Time) self.default_timezone = :utc
Date.send(:include, ValidatesTimeliness::CoreExtensions::Date)
DateTime.send(:include, ValidatesTimeliness::CoreExtensions::DateTime) 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 ]
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
else
I18n.translate('activerecord.errors.messages')
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
raise "Rails version #{Rails::VERSION::STRING} not yet supported by validates_timeliness plugin"
end
end
end
ValidatesTimeliness.setup_for_rails
ValidatesTimeliness::Formats.compile_format_expressions ValidatesTimeliness::Formats.compile_format_expressions

View File

@@ -0,0 +1,45 @@
module ValidatesTimeliness
module ActionView
# Intercepts the date and time select helpers to allow the
# attribute value before type cast to be used as in the select helpers.
# This means that an invalid date or time will be redisplayed rather than the
# type cast value which would be nil if invalid.
module InstanceTag
def self.included(base)
selector_method = Rails::VERSION::STRING < '2.2' ? :date_or_time_select : :datetime_selector
base.class_eval do
alias_method :datetime_selector_without_timeliness, selector_method
alias_method selector_method, :datetime_selector_with_timeliness
end
base.alias_method_chain :value, :timeliness
end
TimelinessDateTime = Struct.new(:year, :month, :day, :hour, :min, :sec)
def datetime_selector_with_timeliness(*args)
@timeliness_date_or_time_tag = true
datetime_selector_without_timeliness(*args)
end
def value_with_timeliness(object)
return value_without_timeliness(object) unless @timeliness_date_or_time_tag
raw_value = value_before_type_cast(object)
if raw_value.nil? || raw_value.acts_like?(:time) || raw_value.is_a?(Date)
return value_without_timeliness(object)
end
time_array = ParseDate.parsedate(raw_value)
TimelinessDateTime.new(*time_array[0..5])
end
end
end
end
ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::ActionView::InstanceTag)

View File

@@ -0,0 +1,157 @@
module ValidatesTimeliness
module ActiveRecord
# Rails 2.1 removed the ability to retrieve the raw value of a time or datetime
# attribute. The raw value is necessary to properly validate a string time or
# datetime value instead of the internal Rails type casting which is very limited
# and does not allow custom formats. These methods restore that ability while
# respecting the automatic timezone handling.
#
# The automatic timezone handling sets the assigned attribute value to the current
# zone in Time.zone. To preserve this localised value and capture the raw value
# we cache the localised value on write and store the raw value in the attributes
# hash for later retrieval and possibly validation. Any value from the database
# will not be in the attribute cache on first read so will be considered in default
# timezone and converted to local time. It is then stored back in the attributes
# hash and cached to avoid the need for any subsequent differentiation.
#
# The wholesale replacement of the Rails time type casting is not done to
# preserve the quickest conversion for timestamp columns and also any value
# which is never changed during the life of the record object.
module AttributeMethods
def self.included(base)
base.extend ClassMethods
if Rails::VERSION::STRING < '2.1'
base.class_eval do
class << self
def create_time_zone_conversion_attribute?(name, column)
false
end
end
end
end
end
# Adds check for cached date/time attributes which have been type cast already
# 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)
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
end
else
nil
end
end
# Writes attribute value by storing raw value in attributes hash,
# then convert it with parser and cache it.
#
# If Rails dirty attributes is enabled then the value is added to
# changed attributes if changed. Can't use the default dirty checking
# implementation as it chains the write_attribute method which deletes
# the attribute from the cache.
def write_date_time_attribute(attr_name, value)
column = column_for_attribute(attr_name)
old = read_attribute(attr_name) if defined?(::ActiveRecord::Dirty)
new = self.class.parse_date_time(value, column.type)
unless column.type == :date || new.nil?
new = new.to_time rescue new
end
if self.class.send(:create_time_zone_conversion_attribute?, attr_name, column)
new = new.in_time_zone rescue nil
end
@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
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
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)
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)
end
end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end
end
# Define write method for date, time and datetime columns
def define_write_method_for_dates_and_times(attr_name)
method_body = <<-EOV
def #{attr_name}=(value)
write_date_time_attribute('#{attr_name}', value)
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)
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
end
EOV
evaluate_attribute_method attr_name, method_body
end
end
end
end
end
ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::AttributeMethods)

View File

@@ -0,0 +1,64 @@
module ValidatesTimeliness
module ActiveRecord
module MultiparameterAttributes
def self.included(base)
base.alias_method_chain :execute_callstack_for_multiparameter_attributes, :timeliness
end
# Overrides AR method to store multiparameter time and dates as string
# allowing validation later.
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)
begin
value = if [:date, :time, :datetime].include?(column.type)
time_array_to_string(values, column.type)
else
klass.new(*values)
end
send(name + "=", value)
rescue => ex
errors << ::ActiveRecord::AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
end
end
end
unless errors.empty?
raise ::ActiveRecord::MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
end
end
def time_array_to_string(values, type)
values = values.map(&: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)
end
end
def extract_date_from_multiparameter_attributes(values)
[values[0], *values.slice(1, 2).map { |s| s.rjust(2, "0") }].join("-")
end
def extract_time_from_multiparameter_attributes(values)
values.last(3).map { |s| s.rjust(2, "0") }.join(":")
end
end
end
end
ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes)

View File

@@ -1,150 +0,0 @@
module ValidatesTimeliness
# The crux of the plugin is being able to store raw user entered values
# while not interfering with the Rails 2.1 automatic timezone handling. This
# requires us to distinguish a user entered value from a value read from the
# database. Both maybe in string form, but only the database value should be
# interpreted as being in the default timezone which is normally UTC. The user
# entered value should be interpreted as being in the current zone as indicated
# by Time.zone.
#
# To do this we must cache the user entered values on write and store the raw
# value in the attributes hash for later retrieval and possibly validation.
# Any value from the database will not be in the attribute cache on first
# read so will be considered in default timezone and converted to local time.
# It is then stored back in the attributes hash and cached to avoid the need
# for any subsequent differentiation.
#
# The wholesale replacement of the Rails time type casting is not done to
# preserve the quickest conversion for timestamp columns and also any value
# which is never changed during the life of the record object.
module AttributeMethods
def self.included(base)
base.extend ClassMethods
if Rails::VERSION::STRING < '2.1'
base.class_eval do
class << self
def create_time_zone_conversion_attribute?(name, column)
false
end
end
end
end
end
# Adds check for cached date/time attributes which have been type cast already
# 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)
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
end
else
nil
end
end
# Writes attribute value by storing raw value in attributes hash,
# then convert it with parser and cache it.
#
# If Rails dirty attributes is enabled then the value is added to
# changed attributes if changed. Can't use the default dirty checking
# implementation as it chains the write_attribute method which deletes
# the attribute from the cache.
def write_date_time_attribute(attr_name, value)
attr_name = attr_name.to_s
column = column_for_attribute(attr_name)
old = read_attribute(attr_name) if defined?(ActiveRecord::Dirty)
new = self.class.parse_date_time(value, column.type)
if self.class.send(:create_time_zone_conversion_attribute?, attr_name, column)
new = new.in_time_zone rescue nil
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
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
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.to_sym)
else
define_read_method(name.to_sym, name, column)
end
end
unless instance_method_already_implemented?("#{name}=")
if [:date, :time, :datetime].include?(column.type)
define_write_method_for_dates_and_times(name.to_sym)
else
define_write_method(name.to_sym)
end
end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end
end
# Define write method for date, time and datetime columns
def define_write_method_for_dates_and_times(attr_name)
method_body = <<-EOV
def #{attr_name}=(value)
write_date_time_attribute('#{attr_name}', value)
end
EOV
evaluate_attribute_method attr_name, method_body
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)
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
end
EOV
evaluate_attribute_method attr_name, method_body
end
end
end
end

View File

@@ -3,9 +3,11 @@ module ValidatesTimeliness
module Date module Date
def to_dummy_time def to_dummy_time
::Time.mktime(2000, 1, 1, 0, 0, 0) ::Time.send(ValidatesTimeliness.default_timezone, 2000, 1, 1, 0, 0, 0)
end end
end end
end end
end end
Date.send(:include, ValidatesTimeliness::CoreExtensions::Date)

View File

@@ -3,9 +3,11 @@ module ValidatesTimeliness
module DateTime module DateTime
def to_dummy_time def to_dummy_time
::Time.mktime(2000, 1, 1, hour, min, sec) ::Time.send(ValidatesTimeliness.default_timezone, 2000, 1, 1, hour, min, sec)
end end
end end
end end
end end
DateTime.send(:include, ValidatesTimeliness::CoreExtensions::DateTime)

View File

@@ -3,9 +3,11 @@ module ValidatesTimeliness
module Time module Time
def to_dummy_time def to_dummy_time
self.class.mktime(2000, 1, 1, hour, min, sec) self.class.send(ValidatesTimeliness.default_timezone, 2000, 1, 1, hour, min, sec)
end end
end end
end end
end end
Time.send(:include, ValidatesTimeliness::CoreExtensions::Time)

View File

@@ -237,8 +237,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

View File

@@ -1,40 +0,0 @@
module ValidatesTimeliness
# Intercepts the date and time select helpers to allow the
# attribute value before type cast to be used as in the select helpers.
# This means that an invalid date or time will be redisplayed rather than the
# type cast value which would be nil if invalid.
module InstanceTag
def self.included(base)
selector_method = Rails::VERSION::STRING < '2.2' ? :date_or_time_select : :datetime_selector
base.class_eval do
alias_method :datetime_selector_without_timeliness, selector_method
alias_method selector_method, :datetime_selector_with_timeliness
end
base.alias_method_chain :value, :timeliness
end
TimelinessDateTime = Struct.new(:year, :month, :day, :hour, :min, :sec)
def datetime_selector_with_timeliness(*args)
@timeliness_date_or_time_tag = true
datetime_selector_without_timeliness(*args)
end
def value_with_timeliness(object)
return value_without_timeliness(object) unless @timeliness_date_or_time_tag
raw_value = value_before_type_cast(object)
if raw_value.nil? || raw_value.acts_like?(:time) || raw_value.is_a?(Date)
return value_without_timeliness(object)
end
time_array = ParseDate.parsedate(raw_value)
TimelinessDateTime.new(*time_array[0..5])
end
end
end

View File

@@ -0,0 +1,12 @@
en:
activerecord:
errors:
messages:
invalid_date: "is not a valid date"
invalid_time: "is not a valid time"
invalid_datetime: "is not a valid datetime"
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}}"

View File

@@ -1,58 +0,0 @@
module ValidatesTimeliness
module MultiparameterAttributes
def self.included(base)
base.alias_method_chain :execute_callstack_for_multiparameter_attributes, :timeliness
end
# Overrides AR method to store multiparameter time and dates as string
# allowing validation later.
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)
begin
value = if [:date, :time, :datetime].include?(column.type)
time_array_to_string(values, column.type)
else
klass.new(*values)
end
send(name + "=", value)
rescue => ex
errors << ActiveRecord::AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
end
end
end
unless errors.empty?
raise ActiveRecord::MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
end
end
def time_array_to_string(values, type)
values = values.map(&: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)
end
end
def extract_date_from_multiparameter_attributes(values)
[values[0], *values.slice(1, 2).map { |s| s.rjust(2, "0") }].join("-")
end
def extract_time_from_multiparameter_attributes(values)
values.last(3).map { |s| s.rjust(2, "0") }.join(":")
end
end
end

View File

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

View File

@@ -1,119 +0,0 @@
module Spec
module Rails
module Matchers
class ValidateTimeliness
def initialize(attribute, options)
@expected, @options = attribute, options
@options.reverse_merge!(error_messages)
end
def matches?(record)
@record = record
type = options[:type]
test_values = {
:date => {:pass => '2000-01-01', :fail => '2000-01-32'},
:time => {:pass => '12:00', :fail => '25:00'},
:datetime => {:pass => '2000-01-01 00:00:00', :fail => '2000-01-32 00:00:00'}
}
invalid_value = test_values[type][:fail]
valid_value = parse_and_cast(test_values[type][:pass])
valid = error_matching(invalid_value, /#{options["invalid_#{type}_message".to_sym]}/) &&
no_error_matching(valid_value, /#{options["invalid_#{type}_message".to_sym]}/)
valid = test_option(:before, :-) if options[:before] && valid
valid = test_option(:after, :+) if options[:after] && valid
valid = test_option(:on_or_before, :+, :modify_on => :invalid) if options[:on_or_before] && valid
valid = test_option(:on_or_after, :-, :modify_on => :invalid) if options[:on_or_after] && valid
return valid
end
def failure_message
"expected model to validate #{options[:type]} attribute #{expected.inspect} with #{last_failure}"
end
def negative_failure_message
"expected not to validate #{options[:type]} attribute #{expected.inspect}"
end
def description
"have validated #{options[:type]} attribute #{expected.inspect}"
end
private
attr_reader :actual, :expected, :record, :options, :last_failure
def test_option(option, modifier, settings={})
settings.reverse_merge!(:modify_on => :valid)
boundary = parse_and_cast(options[option])
valid_value, invalid_value = if settings[:modify_on] == :valid
[ boundary.send(modifier, 1), boundary ]
else
[ boundary, boundary.send(modifier, 1) ]
end
message = options["#{option}_message".to_sym]
error_matching(invalid_value, /#{message}/) &&
no_error_matching(valid_value, /#{message}/)
end
def parse_and_cast(value)
value = ActiveRecord::Base.send(:timeliness_restriction_value, value, record, options[:type])
cast_method = ActiveRecord::Base.send(:restriction_type_cast_method, options[:type])
value.send(cast_method) rescue nil
end
def error_messages
messages = ActiveRecord::Base.send(:timeliness_default_error_messages)
messages = messages.inject({}) {|h, (k, v)| h[k] = v.sub(' %s', ''); h }
@options.reverse_merge!(messages)
end
def error_matching(value, match)
record.send("#{expected}=", value)
record.valid?
errors = record.errors.on(expected)
pass = [ errors ].flatten.any? {|error| match === error }
@last_failure = "error matching #{match.inspect} when value is #{format_value(value)}" unless pass
pass
end
def no_error_matching(value, match)
pass = !error_matching(value, match)
@last_failure = "no error matching #{match.inspect} when value is #{format_value(value)}" unless pass
pass
end
def format_value(value)
return value if value.is_a?(String)
value.strftime(ActiveRecord::Errors.date_time_error_value_formats[options[:type]])
end
end
def validate_date(attribute, options={})
options[:type] = :date
validate_timeliness_of(attribute, options)
end
def validate_time(attribute, options={})
options[:type] = :time
validate_timeliness_of(attribute, options)
end
def validate_datetime(attribute, options={})
options[:type] = :datetime
validate_timeliness_of(attribute, options)
end
private
def validate_timeliness_of(attribute, options={})
ValidateTimeliness.new(attribute, options)
end
end
end
end

View File

@@ -0,0 +1,82 @@
module ValidatesTimeliness
module ValidationMethods
def self.included(base)
base.extend ClassMethods
end
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
validates_timeliness_of(attr_names, configuration)
end
def validates_date(*attr_names)
configuration = attr_names.extract_options!
configuration[:type] = :date
validates_timeliness_of(attr_names, configuration)
end
def validates_datetime(*attr_names)
configuration = attr_names.extract_options!
configuration[:type] = :datetime
validates_timeliness_of(attr_names, configuration)
end
private
def validates_timeliness_of(attr_names, configuration)
validator = ValidatesTimeliness::Validator.new(configuration)
# 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
end
end
end
end
end
ActiveRecord::Base.send(:include, ValidatesTimeliness::ValidationMethods)

View File

@@ -1,189 +0,0 @@
module ValidatesTimeliness
# Adds ActiveRecord validation methods for date, time and datetime validation.
# The validity of values can be restricted to be before or after certain dates
# or times.
module Validations
# Error messages and error value formats added to AR defaults to allow
# global override.
def self.included(base)
base.extend ClassMethods
base.class_inheritable_accessor :ignore_datetime_restriction_errors
base.ignore_datetime_restriction_errors = false
ActiveRecord::Errors.class_inheritable_accessor :date_time_error_value_formats
ActiveRecord::Errors.date_time_error_value_formats = {
:time => '%H:%M:%S',
:date => '%Y-%m-%d',
:datetime => '%Y-%m-%d %H:%M:%S'
}
ActiveRecord::Errors.default_error_messages.update(
:invalid_date => "is not a valid date",
:invalid_time => "is not a valid time",
:invalid_datetime => "is not a valid datetime",
:before => "must be before %s",
:on_or_before => "must be on or before %s",
:after => "must be after %s",
:on_or_after => "must be on or after %s"
)
end
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
# The main validation method which can be used directly or called through
# the other specific type validation methods.
def validates_timeliness_of(*attr_names)
configuration = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
configuration.update(timeliness_default_error_messages)
configuration.update(attr_names.extract_options!)
# we need to check raw value for blank or nil
allow_nil = configuration.delete(:allow_nil)
allow_blank = configuration.delete(:allow_blank)
validates_each(attr_names, configuration) do |record, attr_name, value|
raw_value = record.send("#{attr_name}_before_type_cast")
next if (raw_value.nil? && allow_nil) || (raw_value.blank? && allow_blank)
record.errors.add(attr_name, configuration[:blank_message]) and next if raw_value.blank?
column = record.column_for_attribute(attr_name)
begin
unless time = parse_date_time(raw_value, configuration[:type])
record.errors.add(attr_name, configuration["invalid_#{configuration[:type]}_message".to_sym])
next
end
validate_timeliness_restrictions(record, attr_name, time, configuration)
rescue Exception => e
record.errors.add(attr_name, configuration["invalid_#{configuration[:type]}_message".to_sym])
end
end
end
# Use this validation to force validation of values and restrictions
# as dummy time
def validates_time(*attr_names)
configuration = attr_names.extract_options!
configuration[:type] = :time
validates_timeliness_of(attr_names, configuration)
end
# Use this validation to force validation of values and restrictions
# as Date
def validates_date(*attr_names)
configuration = attr_names.extract_options!
configuration[:type] = :date
validates_timeliness_of(attr_names, configuration)
end
# Use this validation to force validation of values and restrictions
# as Time/DateTime
def validates_datetime(*attr_names)
configuration = attr_names.extract_options!
configuration[:type] = :datetime
validates_timeliness_of(attr_names, configuration)
end
private
def timeliness_restriction_value(restriction, record, type)
case restriction
when Time, Date, DateTime
restriction
when Symbol
timeliness_restriction_value(record.send(restriction), record, type)
when Proc
timeliness_restriction_value(restriction.call(record), record, type)
else
parse_date_time(restriction, type, false)
end
end
def restriction_type_cast_method(type)
case type
when :time then :to_dummy_time
when :date then :to_date
when :datetime then :to_time
end
end
# Validate value against the temporal restrictions. Restriction values
# maybe of mixed type, so they are evaluated as a common type, which may
# require conversion. The type used is defined by validation type.
def validate_timeliness_restrictions(record, attr_name, value, configuration)
restriction_methods = {:before => '<', :after => '>', :on_or_before => '<=', :on_or_after => '>='}
type_cast_method = restriction_type_cast_method(configuration[:type])
display = ActiveRecord::Errors.date_time_error_value_formats[configuration[:type]]
value = value.send(type_cast_method)
restriction_methods.each do |option, method|
next unless restriction = configuration[option]
begin
compare = timeliness_restriction_value(restriction, record, configuration[:type])
next if compare.nil?
compare = compare.send(type_cast_method)
record.errors.add(attr_name, configuration["#{option}_message".to_sym] % compare.strftime(display)) unless value.send(method, compare)
rescue
record.errors.add(attr_name, "restriction '#{option}' value was invalid") unless self.ignore_datetime_restriction_errors
end
end
end
# Map error message keys to *_message to merge with validation options
def timeliness_default_error_messages
defaults = ActiveRecord::Errors.default_error_messages.slice(
:blank, :invalid_date, :invalid_time, :invalid_datetime, :before, :on_or_before, :after, :on_or_after)
returning({}) do |messages|
defaults.each {|k, v| messages["#{k}_message".to_sym] = v }
end
end
# Create time in correct timezone. For Rails 2.1 that is value in
# Time.zone. Rails 2.0 should be default_timezone.
def make_time(time_array)
if Time.respond_to?(:zone) && time_zone_aware_attributes
Time.zone.local(*time_array)
else
begin
Time.send(ActiveRecord::Base.default_timezone, *time_array)
rescue ArgumentError, TypeError
zone_offset = ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0
time_array.pop # remove microseconds
DateTime.civil(*(time_array << zone_offset))
end
end
end
end
end
end

View File

@@ -0,0 +1,163 @@
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 = {
:before => :<,
:after => :>,
:on_or_before => :<=,
:on_or_after => :>=,
:between => lambda {|v, r| (r.first..r.last).include?(v) }
}
attr_reader :configuration, :type
def initialize(configuration)
defaults = { :on => :save, :type => :datetime, :allow_nil => false, :allow_blank => false }
@configuration = defaults.merge(configuration)
@type = @configuration.delete(:type)
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)
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
validate_restrictions(record, attr_name, value)
end
private
def raw_value(record, attr_name)
record.send("#{attr_name}_before_type_cast")
end
def validate_restrictions(record, attr_name, value)
value = type_cast_value(value)
RESTRICTION_METHODS.each do |option, method|
next unless restriction = configuration[option]
begin
restriction = restriction_value(restriction, record)
next if restriction.nil?
restriction = type_cast_value(restriction)
unless evaluate_restriction(restriction, value, method)
add_error(record, attr_name, option, interpolation_values(option, restriction))
end
rescue
unless self.class.ignore_restriction_errors
add_error(record, attr_name, "restriction '#{option}' value was invalid")
end
end
end
end
def interpolation_values(option, restriction)
format = self.class.error_value_formats[type]
restriction = [restriction] unless restriction.is_a?(Array)
if defined?(I18n)
message = custom_error_messages[option] || I18n.translate('activerecord.errors.messages')[option]
subs = message.scan(/\{\{([^\}]*)\}\}/)
interpolations = {}
subs.each_with_index {|s, i| interpolations[s[0].to_sym] = restriction[i].strftime(format) }
interpolations
else
restriction.map {|r| r.strftime(format) }
end
end
def evaluate_restriction(restriction, value, comparator)
return true if restriction.nil?
case comparator
when Symbol
value.send(comparator, restriction)
when Proc
comparator.call(value, restriction)
end
end
def add_error(record, attr_name, message, interpolate=nil)
if defined?(I18n)
# use i18n support in AR for message or use custom message passed to validation method
custom = custom_error_messages[message]
record.errors.add(attr_name, custom || message, interpolate || {})
else
message = error_messages[message] if message.is_a?(Symbol)
message = message % interpolate
record.errors.add(attr_name, message)
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)|
if md = /(.*)_message$/.match(k.to_s)
msgs[md[0].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)
end
end
def type_cast_value(value)
if value.is_a?(Array)
value.map {|v| type_cast_value(v) }
else
case type
when :time
value.to_dummy_time
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
end
end
end
end

View File

@@ -1,6 +1,6 @@
require File.dirname(__FILE__) + '/spec_helper' require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ValidatesTimeliness::InstanceTag, :type => :helper do describe ValidatesTimeliness::ActionView::InstanceTag, :type => :helper do
before do before do
@person = Person.new @person = Person.new

View File

@@ -1,13 +1,28 @@
require File.dirname(__FILE__) + '/spec_helper' require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ValidatesTimeliness::AttributeMethods do describe ValidatesTimeliness::ActiveRecord::AttributeMethods do
include ValidatesTimeliness::AttributeMethods include ValidatesTimeliness::ActiveRecord::AttributeMethods
include ValidatesTimeliness::Validations include ValidatesTimeliness::ValidationMethods
before do before do
@person = Person.new @person = Person.new
end end
it "should call write_date_time_attribute when date attribute assigned value" do
@person.should_receive(:write_date_time_attribute)
@person.birth_date = "2000-01-01"
end
it "should call write_date_time_attribute when time attribute assigned value" do
@person.should_receive(:write_date_time_attribute)
@person.birth_time = "12:00"
end
it "should call write_date_time_attribute when datetime attribute assigned value" do
@person.should_receive(:write_date_time_attribute)
@person.birth_date_and_time = "2000-01-01 12:00"
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"

View File

@@ -1,6 +1,6 @@
require File.dirname(__FILE__) + '/spec_helper' require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ValidatesTimeliness::MultiparameterAttributes do describe ValidatesTimeliness::ActiveRecord::MultiparameterAttributes do
def obj def obj
@obj ||= Person.new @obj ||= Person.new
end end
@@ -39,11 +39,9 @@ describe ValidatesTimeliness::MultiparameterAttributes do
obj.send(:execute_callstack_for_multiparameter_attributes, @callstack) obj.send(:execute_callstack_for_multiparameter_attributes, @callstack)
end end
unless RAILS_VER < '2.1' # sqlite doesn't support :time attribute in rails 2.0.x it "should store time string for a time column" do
it "should store time string for a time column" do obj.should_receive(:birth_time=).once.with("09:10:11")
obj.should_receive(:birth_time=).once.with("09:10:11") obj.send(:execute_callstack_for_multiparameter_attributes, @callstack)
obj.send(:execute_callstack_for_multiparameter_attributes, @callstack)
end
end end
end end

View File

@@ -0,0 +1,31 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ValidatesTimeliness::CoreExtensions::Date do
before do
@a_date = Date.new(2008, 7, 1)
end
it "should make a date value into a dummy time value" do
@a_date.to_dummy_time.should == Time.utc(2000,1,1,0,0,0)
end
end
describe ValidatesTimeliness::CoreExtensions::Time do
before do
@a_time = Time.mktime(2008, 7, 1, 2, 3, 4)
end
it "should make a time value into a dummy time value" do
@a_time.to_dummy_time.should == Time.utc(2000,1,1,2,3,4)
end
end
describe ValidatesTimeliness::CoreExtensions::DateTime do
before do
@a_datetime = DateTime.new(2008, 7, 1, 2, 3, 4)
end
it "should make a datetime value into a dummy time value" do
@a_datetime.to_dummy_time.should == Time.utc(2000,1,1,2,3,4)
end
end

View File

@@ -1,31 +0,0 @@
require File.dirname(__FILE__) + '/spec_helper'
describe ValidatesTimeliness::CoreExtensions::Date do
before do
@a_date = Date.new(2008, 7, 1)
end
it "should " do
@a_date.to_dummy_time.should == Time.mktime(2000,1,1,0,0,0)
end
end
describe ValidatesTimeliness::CoreExtensions::Time do
before do
@a_time = Time.mktime(2008, 7, 1, 2, 3, 4)
end
it "should " do
@a_time.to_dummy_time.should == Time.mktime(2000,1,1,2,3,4)
end
end
describe ValidatesTimeliness::CoreExtensions::DateTime do
before do
@a_datetime = DateTime.new(2008, 7, 1, 2, 3, 4)
end
it "should " do
@a_datetime.to_dummy_time.should == Time.mktime(2000,1,1,2,3,4)
end
end

View File

@@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/spec_helper' require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe ValidatesTimeliness::Formats do describe ValidatesTimeliness::Formats do
attr_reader :formats attr_reader :formats

View File

@@ -0,0 +1,19 @@
# patches adapter in rails 2.0 which mistakenly made time attributes map to datetime column type
ActiveRecord::ConnectionAdapters::SQLiteAdapter.class_eval do
def native_database_types #:nodoc:
{
:primary_key => default_primary_key_type,
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "boolean" }
}
end
end

View File

@@ -1,39 +1,51 @@
require File.dirname(__FILE__) + '/spec_helper' require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
class NoValidation < Person
end
class WithValidation < Person
validates_date :birth_date,
:before => '2000-01-10', :after => '2000-01-01',
:on_or_before => '2000-01-09', :on_or_after => '2000-01-02',
:between => ['2000-01-01', '2000-01-03']
validates_time :birth_time,
:before => '23:00', :after => '09:00',
:on_or_before => '22:00', :on_or_after => '10:00',
:between => ['09:00', '17:00']
validates_datetime :birth_date_and_time,
:before => '2000-01-10 23:00', :after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00', :on_or_after => '2000-01-02 09:00',
:between => ['2000-01-01 09:00', '2000-01-01 17:00']
end
class CustomMessages < Person
validates_date :birth_date, :invalid_date_message => 'is not really a date',
:before => '2000-01-10', :before_message => 'is too late',
:after => '2000-01-01', :after_message => 'is too early',
:on_or_before=> '2000-01-09', :on_or_before_message => 'is just too late',
:on_or_after => '2000-01-02', :on_or_after_message => 'is just too early'
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
class NoValidation < Person
alias_attribute :birth_datetime, :birth_date_and_time
end
class WithValidation < Person
validates_date :birth_date,
:before => '2000-01-10', :after => '2000-01-01',
:on_or_before => '2000-01-09', :on_or_after => '2000-01-02'
validates_time :birth_time,
:before => '23:00', :after => '09:00',
:on_or_before => '22:00', :on_or_after => '10:00'
validates_datetime :birth_date_and_time,
:before => '2000-01-10 23:00', :after => '2000-01-01 09:00',
:on_or_before => '2000-01-09 23:00', :on_or_after => '2000-01-02 09:00'
alias_attribute :birth_datetime, :birth_date_and_time
end
@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
@@ -45,18 +57,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
@@ -69,18 +80,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
@@ -93,18 +103,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
@@ -117,31 +126,47 @@ 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 end
end end
describe "custom messages" do describe "between option" do
before do test_values = {
class CustomMessages < Person :date => [ ['2000-01-01', '2000-01-03'], ['2000-01-01', '2000-01-04'] ],
validates_date :birth_date, :invalid_date_message => 'is not really a date', :time => [ ['09:00', '17:00'], ['09:00', '17:01'] ],
:before => '2000-01-10', :before_message => 'is too late', :datetime => [ ['2000-01-01 09:00', '2000-01-01 17:00'], ['2000-01-01 09:00', '2000-01-01 17:01'] ]
: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' [: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 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
describe "custom messages" do
before do
@person = CustomMessages.new @person = CustomMessages.new
end end
@@ -174,4 +199,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,7 +16,11 @@ else
require 'ginger' require 'ginger'
rescue LoadError rescue LoadError
end end
gem 'rails' if ENV['VERSION']
gem 'rails', ENV['VERSION']
else
gem 'rails'
end
end end
RAILS_ROOT = File.dirname(__FILE__) RAILS_ROOT = File.dirname(__FILE__)
@@ -27,17 +31,14 @@ require 'active_record/version'
require 'action_controller' require 'action_controller'
require 'action_view' require 'action_view'
ActiveSupport::Deprecation.silenced = true
require 'spec/rails' require 'spec/rails'
require 'time_travel/time_travel' require 'time_travel/time_travel'
require 'validates_timeliness'
RAILS_VER = Rails::VERSION::STRING
puts "Using #{vendored ? 'vendored' : 'gem'} Rails version #{RAILS_VER} (ActiveRecord version #{ActiveRecord::VERSION::STRING})"
ActiveRecord::Base.default_timezone = :utc 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' if RAILS_VER >= '2.1'
Time.zone_default = ActiveSupport::TimeZone['UTC'] Time.zone_default = ActiveSupport::TimeZone['UTC']
@@ -47,5 +48,7 @@ end
ActiveRecord::Migration.verbose = false ActiveRecord::Migration.verbose = false
ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'}) ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
require 'sqlite_patch' if RAILS_VER < '2.1'
require 'schema' require 'schema'
require 'person' require 'person'

View File

@@ -0,0 +1,61 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe ValidatesTimeliness::ValidationMethods do
attr_accessor :person
describe "parse_date_time" do
it "should return time object for valid time string" do
parse_method("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
end
it "should return nil for time string with invalid date part" do
parse_method("2000-02-30 12:13:14", :datetime).should be_nil
end
it "should return nil for time string with invalid time part" do
parse_method("2000-02-01 25:13:14", :datetime).should be_nil
end
it "should return Time object when passed a Time object" do
parse_method(Time.now, :datetime).should be_kind_of(Time)
end
if RAILS_VER >= '2.1'
it "should convert time string into current timezone" do
Time.zone = 'Melbourne'
time = parse_method("2000-01-01 12:13:14", :datetime)
Time.zone.utc_offset.should == 10.hours
end
end
it "should return nil for invalid date string" do
parse_method("2000-02-30", :date).should be_nil
end
def parse_method(*args)
ActiveRecord::Base.parse_date_time(*args)
end
end
describe "make_time" do
if RAILS_VER >= '2.1'
it "should create time using current timezone" do
Time.zone = 'Melbourne'
time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0])
time.zone.should == "EST"
end
else
it "should create time using default timezone" do
time = ActiveRecord::Base.send(:make_time, [2000,1,1,12,0,0])
time.zone.should == "UTC"
end
end
end
end

View File

@@ -1,507 +0,0 @@
require File.dirname(__FILE__) + '/spec_helper'
describe ValidatesTimeliness::Validations do
attr_accessor :person
before :all do
# freezes time using time_travel plugin
Time.now = Time.utc(2000, 1, 1, 0, 0, 0)
end
after :all do
Time.now = nil
end
describe "parse_date_time" do
it "should return time object for valid time string" do
parse_method("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
end
it "should return nil for time string with invalid date part" do
parse_method("2000-02-30 12:13:14", :datetime).should be_nil
end
it "should return nil for time string with invalid time part" do
parse_method("2000-02-01 25:13:14", :datetime).should be_nil
end
it "should return Time object when passed a Time object" do
parse_method(Time.now, :datetime).should be_kind_of(Time)
end
if RAILS_VER >= '2.1'
it "should convert time string into current timezone" do
Time.zone = 'Melbourne'
time = parse_method("2000-01-01 12:13:14", :datetime)
Time.zone.utc_offset.should == 10.hours
end
end
it "should return nil for invalid date string" do
parse_method("2000-02-30", :date).should be_nil
end
def parse_method(*args)
ActiveRecord::Base.parse_date_time(*args)
end
end
describe "timeliness_restriction_value" do
it "should return Time object when restriction is Time object" do
restriction_value(Time.now, person, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is string" do
restriction_value("2007-01-01 12:00", person, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is method symbol which returns Time object" do
person.stub!(:datetime_attr).and_return(Time.now)
restriction_value(:datetime_attr, person, :datetime).should be_kind_of(Time)
end
it "should return Time object when restriction is method symbol which returns string" do
person.stub!(:datetime_attr).and_return("2007-01-01 12:00")
restriction_value(:datetime_attr, person, :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 }, person, :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"}, person, :datetime).should be_kind_of(Time)
end
def restriction_value(*args)
ActiveRecord::Base.send(:timeliness_restriction_value, *args)
end
end
describe "with no restrictions" do
before :all do
class BasicValidation < Person
validates_datetime :birth_date_and_time, :allow_blank => true
validates_date :birth_date, :allow_blank => true
validates_time :birth_time, :allow_blank => true
end
end
before :each do
@person = BasicValidation.new
end
it "should have error for invalid date component for datetime column" do
person.birth_date_and_time = "2000-01-32 01:02:03"
person.should_not be_valid
person.errors.on(:birth_date_and_time).should == "is not a valid datetime"
end
it "should have error for invalid time component for datetime column" do
person.birth_date_and_time = "2000-01-01 25:02:03"
person.should_not be_valid
person.errors.on(:birth_date_and_time).should == "is not a valid datetime"
end
it "should have error for invalid date value for date column" do
person.birth_date = "2000-01-32"
person.should_not be_valid
person.errors.on(:birth_date).should == "is not a valid date"
end
it "should have error for invalid time value for time column" do
person.birth_time = "25:00"
person.should_not be_valid
person.errors.on(:birth_time).should == "is not a valid time"
end
it "should have same value for before_type_cast after failed validation" do
person.birth_date_and_time = "2000-01-01 25:02:03"
person.should_not be_valid
person.birth_date_and_time_before_type_cast.should == "2000-01-01 25:02:03"
end
it "should be valid with valid values" do
person.birth_date_and_time = "2000-01-01 12:12:12"
person.birth_date = "2000-01-31"
person.should be_valid
end
it "should be valid with value out of range for Time class" do
person.birth_date_and_time = "1890-01-01 12:12:12"
person.should be_valid
end
it "should be valid with nil values when allow_blank is true" do
person.birth_date_and_time = nil
person.birth_date = nil
person.birth_time = nil
person.should be_valid
end
end
describe "for datetime type" do
describe "with before and after restrictions" do
before :all do
class DateTimeBeforeAfter < Person
validates_datetime :birth_date_and_time,
:before => lambda { Time.now }, :after => lambda { 1.day.ago}
end
end
before :each do
@person = DateTimeBeforeAfter.new
end
it "should have error when past :before restriction" do
person.birth_date_and_time = 1.minute.from_now
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be before/)
end
it "should have error when before :after restriction" do
person.birth_date_and_time = 2.days.ago
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be after/)
end
it "should have error when on boundary of :before restriction" do
person.birth_date_and_time = Time.now
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be before/)
end
it "should have error when on boundary of :after restriction" do
person.birth_date_and_time = 1.day.ago
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be after/)
end
end
describe "with on_or_before and on_or_after restrictions" do
before :all do
class DateTimeOnOrBeforeAndAfter < Person
validates_datetime :birth_date_and_time, :type => :datetime,
:on_or_before => lambda { Time.now.at_midnight },
:on_or_after => lambda { 1.day.ago }
end
end
before do
@person = DateTimeOnOrBeforeAndAfter.new
end
it "should have error when past :on_or_before restriction" do
person.birth_date_and_time = Time.now.at_midnight + 1
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be on or before/)
end
it "should have error when before :on_or_after restriction" do
person.birth_date_and_time = 1.days.ago - 1
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be on or after/)
end
it "should be valid when value equal to :on_or_before restriction" do
person.birth_date_and_time = Time.now.at_midnight
person.should be_valid
end
it "should be valid when value equal to :on_or_after restriction" do
person.birth_date_and_time = 1.day.ago
person.should be_valid
end
end
end
describe "for date type" do
describe "with before and after restrictions" do
before :all do
class DateBeforeAfter < Person
validates_date :birth_date,
:before => 1.day.from_now,
:after => 1.day.ago
end
end
before :each do
@person = DateBeforeAfter.new
end
it "should have error when past :before restriction" do
person.birth_date = 2.days.from_now
person.should_not be_valid
person.errors.on(:birth_date).should match(/must be before/)
end
it "should have error when before :after restriction" do
person.birth_date = 2.days.ago
person.should_not be_valid
person.errors.on(:birth_date).should match(/must be after/)
end
end
describe "with on_or_before and on_or_after restrictions" do
before :all do
class DateOnOrBeforeAndAfter < Person
validates_date :birth_date,
:on_or_before => 1.day.from_now,
:on_or_after => 1.day.ago
end
end
before :each do
@person = DateOnOrBeforeAndAfter.new
end
it "should have error when past :on_or_before restriction" do
person.birth_date = 2.days.from_now
person.should_not be_valid
person.errors.on(:birth_date).should match(/must be on or before/)
end
it "should have error when before :on_or_after restriction" do
person.birth_date = 2.days.ago
person.should_not be_valid
person.errors.on(:birth_date).should match(/must be on or after/)
end
it "should be valid when value equal to :on_or_before restriction" do
person.birth_date = 1.day.from_now
person.should be_valid
end
it "should be valid when value equal to :on_or_after restriction" do
person.birth_date = 1.day.ago
person.should be_valid
end
end
end
describe "for time type" do
describe "with before and after restrictions" do
before :all do
class TimeBeforeAfter < Person
validates_time :birth_time,
:before => "23:00",
:after => "06:00"
end
end
before :each do
@person = TimeBeforeAfter.new
end
it "should have error when on boundary of :before restriction" do
person.birth_time = "23:00"
person.should_not be_valid
person.errors.on(:birth_time).should match(/must be before/)
end
it "should have error when on boundary of :after restriction" do
person.birth_time = "06:00am"
person.should_not be_valid
person.errors.on(:birth_time).should match(/must be after/)
end
it "should have error when past :before restriction" do
person.birth_time = "23:01"
person.should_not be_valid
person.errors.on(:birth_time).should match(/must be before/)
end
it "should have error when before :after restriction" do
person.birth_time = "05:59"
person.should_not be_valid
person.errors.on(:birth_time).should match(/must be after/)
end
it "should not have error when before :before restriction" do
person.birth_time = "22:59"
person.should be_valid
end
it "should have error when before :after restriction" do
person.birth_time = "06:01"
person.should be_valid
end
end
describe "with on_or_before and on_or_after restrictions" do
before :all do
class TimeOnOrBeforeAndAfter < Person
validates_time :birth_time,
:on_or_before => "23:00",
:on_or_after => "06:00"
end
end
before :each do
@person = TimeOnOrBeforeAndAfter.new
end
it "should have error when past :on_or_before restriction" do
person.birth_time = "23:01"
person.should_not be_valid
person.errors.on(:birth_time).should match(/must be on or before/)
end
it "should have error when before :on_or_after restriction" do
person.birth_time = "05:59"
person.should_not be_valid
person.errors.on(:birth_time).should match(/must be on or after/)
end
it "should be valid when on boundary of :on_or_before restriction" do
person.birth_time = "23:00"
person.should be_valid
end
it "should be valid when on boundary of :on_or_after restriction" do
person.birth_time = "06:00"
person.should be_valid
end
end
end
describe "with mixed value and restriction types" do
before :all do
class MixedBeforeAndAfter < Person
validates_datetime :birth_date_and_time,
:before => Date.new(2000,1,2),
:after => lambda { "2000-01-01" }
validates_date :birth_date,
:on_or_before => lambda { "2000-01-01" },
:on_or_after => :birth_date_and_time
end
end
before :each do
@person = MixedBeforeAndAfter.new
end
it "should correctly validate time attribute with Date restriction" do
person.birth_date_and_time = "2000-01-03 00:00:00"
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be before/)
end
it "should correctly validate with proc restriction" do
person.birth_date_and_time = "2000-01-01 00:00:00"
person.should_not be_valid
person.errors.on(:birth_date_and_time).should match(/must be after/)
end
it "should correctly validate date attribute with DateTime restriction" do
person.birth_date = "2000-01-03"
person.birth_date_and_time = "1890-01-01 00:00:00"
person.should_not be_valid
person.errors.on(:birth_date).should match(/must be on or before/)
end
it "should correctly validate date attribute with symbol restriction" do
person.birth_date = "2000-01-01"
person.birth_date_and_time = "2000-01-02 12:00:00"
person.should_not be_valid
person.errors.on(:birth_date).should match(/must be on or after/)
end
end
describe "ignoring restriction errors" do
before :all do
class BadRestriction < Person
validates_date :birth_date, :before => Proc.new { raise }
self.ignore_datetime_restriction_errors = true
end
end
before :each do
@person = BadRestriction.new
end
it "should have no errors when restriction is invalid" do
person.birth_date = '2000-01-01'
person.should be_valid
end
end
describe "restriction value error message" do
describe "default formats" do
before :all do
class DefaultFormats < Person
validates_datetime :birth_date_and_time, :allow_blank => true, :after => 1.day.from_now
validates_date :birth_date, :allow_blank => true, :after => 1.day.from_now
validates_time :birth_time, :allow_blank => true, :after => '23:59:59'
end
end
before :each do
@person = DefaultFormats.new
end
it "should format datetime value of restriction" do
person.birth_date_and_time = Time.now
person.save
person.errors.on(:birth_date_and_time).should match(/after \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\Z/)
end
it "should format date value of restriction" do
person.birth_date = Time.now
person.save
person.errors.on(:birth_date).should match(/after \d{4}-\d{2}-\d{2}\Z/)
end
it "should format time value of restriction" do
person.birth_time = '12:00:00'
person.save
person.errors.on(:birth_time).should match(/after \d{2}:\d{2}:\d{2}\Z/)
end
end
describe "custom formats" do
before :all do
class CustomFormats < Person
validates_datetime :birth_date_and_time, :allow_blank => true, :after => 1.day.from_now
validates_date :birth_date, :allow_blank => true, :after => 1.day.from_now
validates_time :birth_time, :allow_blank => true, :after => '23:59:59'
end
ActiveRecord::Errors.date_time_error_value_formats = {
:time => '%H:%M %p',
:date => '%d-%m-%Y',
:datetime => '%d-%m-%Y %H:%M %p'
}
end
before :each do
@person = CustomFormats.new
end
it "should format datetime value of restriction" do
person.birth_date_and_time = Time.now
person.save
person.errors.on(:birth_date_and_time).should match(/after \d{2}-\d{2}-\d{4} \d{2}:\d{2} (AM|PM)\Z/)
end
it "should format date value of restriction" do
person.birth_date = Time.now
person.save
person.errors.on(:birth_date).should match(/after \d{2}-\d{2}-\d{4}\Z/)
end
it "should format time value of restriction" do
person.birth_time = '12:00:00'
person.save
person.errors.on(:birth_time).should match(/after \d{2}:\d{2} (AM|PM)\Z/)
end
end
end
end

475
spec/validator_spec.rb Normal file
View File

@@ -0,0 +1,475 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe ValidatesTimeliness::Validator do
attr_accessor :person, :validator
before :all do
# freezes time using time_travel plugin
Time.now = Time.utc(2000, 1, 1, 0, 0, 0)
end
after :all do
Time.now = nil
end
before :each do
@person = Person.new
end
describe "restriction_value" do
it "should return Time object when restriction is Time object" do
restriction_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)
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)
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)
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)
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)
end
it "should return array of Time objects when restriction is array of Time objects" do
time1, time2 = Time.now, 1.day.ago
restriction_value([time1, time2], :datetime).should == [time2, time1]
end
it "should return array of Time objects when restriction is array of strings" do
time1, time2 = "2000-01-02", "2000-01-01"
restriction_value([time1, time2], :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
end
it "should return array of Time objects when restriction is Range of Time objects" do
time1, time2 = Time.now, 1.day.ago
restriction_value(time1..time2, :datetime).should == [time2, time1]
end
it "should return array of Time objects when restriction is Range of time strings" do
time1, time2 = "2000-01-02", "2000-01-01"
restriction_value(time1..time2, :datetime).should == [Person.parse_date_time(time2, :datetime), Person.parse_date_time(time1, :datetime)]
end
def restriction_value(restriction, type)
configure_validator(:type => type)
validator.send(:restriction_value, restriction, person)
end
end
describe "instance with defaults" do
describe "for datetime type" do
before do
configure_validator(:type => :datetime)
end
it "should have invalid error when date component is invalid" do
validate_with(:birth_date_and_time, "2000-01-32 01:02:03")
should_have_error(:birth_date_and_time, :invalid_datetime)
end
it "should have invalid error when time component is invalid" do
validate_with(:birth_date_and_time, "2000-01-01 25:02:03")
should_have_error(:birth_date_and_time, :invalid_datetime)
end
it "should have blank error when value is nil" do
validate_with(:birth_date_and_time, nil)
should_have_error(:birth_date_and_time, :blank)
end
it "should have no errors when value is valid" do
validate_with(:birth_date_and_time, "2000-01-01 12:00:00")
should_have_no_error(:birth_date_and_time, :invalid_datetime)
end
end
describe "for date type" do
before do
configure_validator(:type => :date)
end
it "should have invalid error when value is invalid" do
validate_with(:birth_date, "2000-01-32")
should_have_error(:birth_date, :invalid_date)
end
it "should have blank error when value is nil" do
validate_with(:birth_date, nil)
should_have_error(:birth_date, :blank)
end
it "should have no error when value is valid" do
validate_with(:birth_date, "2000-01-31")
should_have_no_error(:birth_date, :invalid_date)
end
end
describe "for time type" do
before do
configure_validator(:type => :time)
end
it "should have invalid error when value is invalid" do
validate_with(:birth_time, "25:00")
should_have_error(:birth_time, :invalid_time)
end
it "should have blank error when value is nil" do
validate_with(:birth_time, nil)
should_have_error(:birth_time, :blank)
end
it "should have no errors when value is valid" do
validate_with(:birth_date_and_time, "12:00")
should_have_no_error(:birth_time, :invalid_time)
end
end
end
describe "instance with before and after restrictions" do
describe "for datetime type" do
before :each do
configure_validator(:before => lambda { Time.now }, :after => lambda { 1.day.ago})
end
it "should have before error when value is past :before restriction" do
validate_with(:birth_date_and_time, 1.minute.from_now)
should_have_error(:birth_date_and_time, :before)
end
it "should have before error when value is on boundary of :before restriction" do
validate_with(:birth_date_and_time, Time.now)
should_have_error(:birth_date_and_time, :before)
end
it "should have after error when value is before :after restriction" do
validate_with(:birth_date_and_time, 2.days.ago)
should_have_error(:birth_date_and_time, :after)
end
it "should have after error when value is on boundary of :after restriction" do
validate_with(:birth_date_and_time, 1.day.ago)
should_have_error(:birth_date_and_time, :after)
end
end
describe "for date type" do
before :each do
configure_validator(:before => 1.day.from_now, :after => 1.day.ago, :type => :date)
end
it "should have error when value is past :before restriction" do
validate_with(:birth_date, 2.days.from_now)
should_have_error(:birth_date, :before)
end
it "should have error when value is before :after restriction" do
validate_with(:birth_date, 2.days.ago)
should_have_error(:birth_date, :after)
end
it "should have no error when value is before :before restriction" do
validate_with(:birth_date, Time.now)
should_have_no_error(:birth_date, :before)
end
it "should have no error when value is after :after restriction" do
validate_with(:birth_date, Time.now)
should_have_no_error(:birth_date, :after)
end
end
describe "for time type" do
before :each do
configure_validator(:before => "23:00", :after => "06:00", :type => :time)
end
it "should have error when value is on boundary of :before restriction" do
validate_with(:birth_time, "23:00")
should_have_error(:birth_time, :before)
end
it "should have error when value is on boundary of :after restriction" do
validate_with(:birth_time, "06:00")
should_have_error(:birth_time, :after)
end
it "should have error when value is past :before restriction" do
validate_with(:birth_time, "23:01")
should_have_error(:birth_time, :before)
end
it "should have error when value is before :after restriction" do
validate_with(:birth_time, "05:59")
should_have_error(:birth_time, :after)
end
it "should not have error when value is before :before restriction" do
validate_with(:birth_time, "22:59")
should_have_no_error(:birth_time, :before)
end
it "should have error when value is before :after restriction" do
validate_with(:birth_time, "06:01")
should_have_no_error(:birth_time, :before)
end
end
end
describe "instance with between restriction" do
describe "for datetime type" do
before do
configure_validator(:between => [1.day.ago.at_midnight, 1.day.from_now.at_midnight])
end
it "should have error when value is before earlist :between restriction" do
validate_with(:birth_date_and_time, 2.days.ago)
should_have_error(:birth_date_and_time, :between)
end
it "should have error when value is after latest :between restriction" do
validate_with(:birth_date_and_time, 2.days.from_now)
should_have_error(:birth_date_and_time, :between)
end
it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_date_and_time, 1.day.ago.at_midnight)
should_have_no_error(:birth_date_and_time, :between)
end
it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_date_and_time, 1.day.from_now.at_midnight)
should_have_no_error(:birth_date_and_time, :between)
end
it "should allow a range for between restriction" do
configure_validator(:type => :datetime, :between => (1.day.ago.at_midnight)..(1.day.from_now.at_midnight))
validate_with(:birth_date_and_time, 1.day.from_now.at_midnight)
should_have_no_error(:birth_date_and_time, :between)
end
end
describe "for date type" do
before do
configure_validator(:type => :date, :between => [1.day.ago.to_date, 1.day.from_now.to_date])
end
it "should have error when value is before earlist :between restriction" do
validate_with(:birth_date, 2.days.ago.to_date)
should_have_error(:birth_date, :between)
end
it "should have error when value is after latest :between restriction" do
validate_with(:birth_date, 2.days.from_now.to_date)
should_have_error(:birth_date, :between)
end
it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_date, 1.day.ago.to_date)
should_have_no_error(:birth_date, :between)
end
it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_date, 1.day.from_now.to_date)
should_have_no_error(:birth_date, :between)
end
it "should allow a range for between restriction" do
configure_validator(:type => :date, :between => (1.day.ago.to_date)..(1.day.from_now.to_date))
validate_with(:birth_date, 1.day.from_now.to_date)
should_have_no_error(:birth_date, :between)
end
end
describe "for time type" do
before do
configure_validator(:type => :time, :between => ["09:00", "17:00"])
end
it "should have error when value is before earlist :between restriction" do
validate_with(:birth_time, "08:59")
should_have_error(:birth_time, :between)
end
it "should have error when value is after latest :between restriction" do
validate_with(:birth_time, "17:01")
should_have_error(:birth_time, :between)
end
it "should be valid when value is equal to earliest :between restriction" do
validate_with(:birth_time, "09:00")
should_have_no_error(:birth_time, :between)
end
it "should be valid when value is equal to latest :between restriction" do
validate_with(:birth_time, "17:00")
should_have_no_error(:birth_time, :between)
end
it "should allow a range for between restriction" do
configure_validator(:type => :time, :between => "09:00".."17:00")
validate_with(:birth_time, "17:00")
should_have_no_error(:birth_time, :between)
end
end
end
describe "instance with mixed value and restriction types" do
it "should validate datetime attribute with Date restriction" do
configure_validator(:type => :datetime, :on_or_before => Date.new(2000,1,1))
validate_with(:birth_date_and_time, "2000-01-01 00:00:00")
should_have_no_error(:birth_date_and_time, :on_or_before)
end
it "should validate date attribute with DateTime restriction value" do
configure_validator(:type => :date, :on_or_before => DateTime.new(2000, 1, 1, 0,0,0))
validate_with(:birth_date, "2000-01-01")
should_have_no_error(:birth_date, :on_or_before)
end
it "should validate date attribute with Time restriction value" do
configure_validator(:type => :date, :on_or_before => Time.utc(2000, 1, 1, 0,0,0))
validate_with(:birth_date, "2000-01-01")
should_have_no_error(:birth_date, :on_or_before)
end
it "should validate time attribute with DateTime restriction value" do
configure_validator(:type => :time, :on_or_before => DateTime.new(2000, 1, 1, 12,0,0))
validate_with(:birth_time, "12:00")
should_have_no_error(:birth_time, :on_or_before)
end
it "should validate time attribute with Time restriction value" do
configure_validator(:type => :time, :on_or_before => Time.utc(2000, 1, 1, 12,0,0))
validate_with(:birth_time, "12:00")
should_have_no_error(:birth_time, :on_or_before)
end
end
describe "restriction errors" do
before :each do
configure_validator(:type => :date, :before => lambda { raise })
end
it "should be added by default for invalid restriction" do
ValidatesTimeliness::Validator.ignore_restriction_errors = false
validate_with(:birth_date, Date.today)
person.errors.on(:birth_date).should match(/restriction 'before' value was invalid/)
end
it "should not be added when ignore switch is true and restriction is invalid" do
ValidatesTimeliness::Validator.ignore_restriction_errors = true
person.should be_valid
end
after :all do
ValidatesTimeliness::Validator.ignore_restriction_errors = false
end
end
describe "restriction value error message" do
describe "default formats" do
it "should format datetime value of restriction" do
configure_validator(:type => :datetime, :after => 1.day.from_now)
validate_with(:birth_date_and_time, Time.now)
person.errors.on(:birth_date_and_time).should match(/after \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\Z/)
end
it "should format date value of restriction" do
configure_validator(:type => :date, :after => 1.day.from_now)
validate_with(:birth_date, Time.now)
person.errors.on(:birth_date).should match(/after \d{4}-\d{2}-\d{2}\Z/)
end
it "should format time value of restriction" do
configure_validator(:type => :time, :after => '12:00')
validate_with(:birth_time, '11:59')
person.errors.on(:birth_time).should match(/after \d{2}:\d{2}:\d{2}\Z/)
end
end
describe "custom formats" do
before :all do
@@formats = ValidatesTimeliness::Validator.error_value_formats
ValidatesTimeliness::Validator.error_value_formats = {
:time => '%H:%M %p',
:date => '%d-%m-%Y',
:datetime => '%d-%m-%Y %H:%M %p'
}
end
it "should format datetime value of restriction" do
configure_validator(:type => :datetime, :after => 1.day.from_now)
validate_with(:birth_date_and_time, Time.now)
person.errors.on(:birth_date_and_time).should match(/after \d{2}-\d{2}-\d{4} \d{2}:\d{2} (AM|PM)\Z/)
end
it "should format date value of restriction" do
configure_validator(:type => :date, :after => 1.day.from_now)
validate_with(:birth_date, Time.now)
person.errors.on(:birth_date).should match(/after \d{2}-\d{2}-\d{4}\Z/)
end
it "should format time value of restriction" do
configure_validator(:type => :time, :after => '12:00')
validate_with(:birth_time, '11:59')
person.errors.on(:birth_time).should match(/after \d{2}:\d{2} (AM|PM)\Z/)
end
after :all do
ValidatesTimeliness::Validator.error_value_formats = @@formats
end
end
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)
end
def should_have_error(attr_name, error)
message = error_messages[error]
person.errors.on(attr_name).should match(/#{message}/)
end
def should_have_no_error(attr_name, error)
message = error_messages[error]
errors = person.errors.on(attr_name)
if errors
errors.should_not match(/#{message}/)
else
errors.should be_nil
end
end
def error_messages
return @error_messages if defined?(@error_messages)
messages = validator.send(:error_messages)
@error_messages = messages.inject({}) {|h, (k, v)| h[k] = v.sub(/ (\%s|\{\{\w*\}\}).*/, ''); h }
end
end

View File

@@ -0,0 +1,31 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{validates_timeliness}
s.version = "1.1.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-01}
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/active_record", "lib/validates_timeliness/active_record/multiparameter_attributes.rb", "lib/validates_timeliness/active_record/attribute_methods.rb", "lib/validates_timeliness/core_ext", "lib/validates_timeliness/core_ext/date.rb", "lib/validates_timeliness/core_ext/time.rb", "lib/validates_timeliness/core_ext/date_time.rb", "lib/validates_timeliness/validator.rb", "lib/validates_timeliness/validation_methods.rb", "lib/validates_timeliness/locale", "lib/validates_timeliness/locale/en.yml", "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/action_view", "lib/validates_timeliness/action_view/instance_tag.rb", "lib/validates_timeliness/formats.rb", "lib/validates_timeliness.rb", "spec/active_record", "spec/active_record/multiparameter_attributes_spec.rb", "spec/active_record/attribute_methods_spec.rb", "spec/validation_methods_spec.rb", "spec/formats_spec.rb", "spec/core_ext", "spec/core_ext/dummy_time_spec.rb", "spec/spec_helper.rb", "spec/ginger_scenarios.rb", "spec/time_travel", "spec/time_travel/time_extensions.rb", "spec/time_travel/time_travel.rb", "spec/time_travel/MIT-LICENSE", "spec/spec", "spec/spec/rails", "spec/spec/rails/matchers", "spec/spec/rails/matchers/validate_timeliness_spec.rb", "spec/validator_spec.rb", "spec/action_view", "spec/action_view/instance_tag_spec.rb", "spec/resources", "spec/resources/schema.rb", "spec/resources/application.rb", "spec/resources/person.rb", "spec/resources/sqlite_patch.rb"]
s.has_rdoc = true
s.homepage = %q{http://github.com/adzap/validates_timeliness}
s.require_paths = ["lib"]
s.rubyforge_project = %q{validatestime}
s.rubygems_version = %q{1.3.1}
s.summary = %q{Date and time validation plugin for Rails 2.x which allows custom formats}
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 2
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
else
end
else
end
end