diff --git a/lib/validates_timeliness.rb b/lib/validates_timeliness.rb index ffcb573..558e46d 100644 --- a/lib/validates_timeliness.rb +++ b/lib/validates_timeliness.rb @@ -1,18 +1,19 @@ -require 'validates_timeliness/attribute_methods' require 'validates_timeliness/validations' require 'validates_timeliness/formats' -require 'validates_timeliness/multiparameter_attributes' -require 'validates_timeliness/instance_tag' require 'validates_timeliness/validate_timeliness_matcher' 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/date' require 'validates_timeliness/core_ext/date_time' -ActiveRecord::Base.send(:include, ValidatesTimeliness::AttributeMethods) ActiveRecord::Base.send(:include, ValidatesTimeliness::Validations) -ActiveRecord::Base.send(:include, ValidatesTimeliness::MultiparameterAttributes) -ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::InstanceTag) +ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::AttributeMethods) +ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes) +ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::ActionView::InstanceTag) Time.send(:include, ValidatesTimeliness::CoreExtensions::Time) Date.send(:include, ValidatesTimeliness::CoreExtensions::Date) diff --git a/lib/validates_timeliness/action_view/instance_tag.rb b/lib/validates_timeliness/action_view/instance_tag.rb new file mode 100644 index 0000000..421fda2 --- /dev/null +++ b/lib/validates_timeliness/action_view/instance_tag.rb @@ -0,0 +1,43 @@ +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 diff --git a/lib/validates_timeliness/active_record/attribute_methods.rb b/lib/validates_timeliness/active_record/attribute_methods.rb new file mode 100644 index 0000000..cde0467 --- /dev/null +++ b/lib/validates_timeliness/active_record/attribute_methods.rb @@ -0,0 +1,153 @@ +module ValidatesTimeliness + module ActiveRecord + + # 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 +end diff --git a/lib/validates_timeliness/active_record/multiparameter_attributes.rb b/lib/validates_timeliness/active_record/multiparameter_attributes.rb new file mode 100644 index 0000000..10b13b5 --- /dev/null +++ b/lib/validates_timeliness/active_record/multiparameter_attributes.rb @@ -0,0 +1,62 @@ +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 diff --git a/lib/validates_timeliness/attribute_methods.rb b/lib/validates_timeliness/attribute_methods.rb deleted file mode 100644 index dc92b8a..0000000 --- a/lib/validates_timeliness/attribute_methods.rb +++ /dev/null @@ -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 diff --git a/lib/validates_timeliness/instance_tag.rb b/lib/validates_timeliness/instance_tag.rb deleted file mode 100644 index 4bc1ef5..0000000 --- a/lib/validates_timeliness/instance_tag.rb +++ /dev/null @@ -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 diff --git a/lib/validates_timeliness/multiparameter_attributes.rb b/lib/validates_timeliness/multiparameter_attributes.rb deleted file mode 100644 index a14d33c..0000000 --- a/lib/validates_timeliness/multiparameter_attributes.rb +++ /dev/null @@ -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 diff --git a/lib/validates_timeliness/validations.rb b/lib/validates_timeliness/validations.rb index b09fea3..b183c1d 100644 --- a/lib/validates_timeliness/validations.rb +++ b/lib/validates_timeliness/validations.rb @@ -12,14 +12,14 @@ module ValidatesTimeliness 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 = { + ::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( + ::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", @@ -140,7 +140,7 @@ module ValidatesTimeliness type_cast_method = restriction_type_cast_method(configuration[:type]) - display = ActiveRecord::Errors.date_time_error_value_formats[configuration[:type]] + display = ::ActiveRecord::Errors.date_time_error_value_formats[configuration[:type]] value = value.send(type_cast_method) @@ -161,7 +161,7 @@ module ValidatesTimeliness # Map error message keys to *_message to merge with validation options def timeliness_default_error_messages - defaults = ActiveRecord::Errors.default_error_messages.slice( + 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 } @@ -175,9 +175,9 @@ module ValidatesTimeliness Time.zone.local(*time_array) else begin - Time.send(ActiveRecord::Base.default_timezone, *time_array) + Time.send(::ActiveRecord::Base.default_timezone, *time_array) rescue ArgumentError, TypeError - zone_offset = ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0 + zone_offset = ::ActiveRecord::Base.default_timezone == :local ? DateTime.local_offset : 0 time_array.pop # remove microseconds DateTime.civil(*(time_array << zone_offset)) end diff --git a/spec/instance_tag_spec.rb b/spec/action_view/instance_tag_spec.rb similarity index 90% rename from spec/instance_tag_spec.rb rename to spec/action_view/instance_tag_spec.rb index 409b24b..b4852ed 100644 --- a/spec/instance_tag_spec.rb +++ b/spec/action_view/instance_tag_spec.rb @@ -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 @person = Person.new diff --git a/spec/attribute_methods_spec.rb b/spec/active_record/attribute_methods_spec.rb similarity index 97% rename from spec/attribute_methods_spec.rb rename to spec/active_record/attribute_methods_spec.rb index 9ad11dc..d6d582c 100644 --- a/spec/attribute_methods_spec.rb +++ b/spec/active_record/attribute_methods_spec.rb @@ -1,7 +1,7 @@ -require File.dirname(__FILE__) + '/spec_helper' +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') -describe ValidatesTimeliness::AttributeMethods do - include ValidatesTimeliness::AttributeMethods +describe ValidatesTimeliness::ActiveRecord::AttributeMethods do + include ValidatesTimeliness::ActiveRecord::AttributeMethods include ValidatesTimeliness::Validations before do diff --git a/spec/multiparameter_attributes_spec.rb b/spec/active_record/multiparameter_attributes_spec.rb similarity index 92% rename from spec/multiparameter_attributes_spec.rb rename to spec/active_record/multiparameter_attributes_spec.rb index e5f7318..0c222d0 100644 --- a/spec/multiparameter_attributes_spec.rb +++ b/spec/active_record/multiparameter_attributes_spec.rb @@ -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 @obj ||= Person.new end diff --git a/spec/dummy_time_spec.rb b/spec/dummy_time_spec.rb index dc96f50..4a147d4 100644 --- a/spec/dummy_time_spec.rb +++ b/spec/dummy_time_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe ValidatesTimeliness::CoreExtensions::Date do before do diff --git a/spec/formats_spec.rb b/spec/formats_spec.rb index d3b691d..10a1c97 100644 --- a/spec/formats_spec.rb +++ b/spec/formats_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe ValidatesTimeliness::Formats do attr_reader :formats diff --git a/spec/validate_timeliness_matcher_spec.rb b/spec/validate_timeliness_matcher_spec.rb index 2c90d4d..0f7dac5 100644 --- a/spec/validate_timeliness_matcher_spec.rb +++ b/spec/validate_timeliness_matcher_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe "ValidateTimeliness matcher" do attr_accessor :no_validation, :with_validation diff --git a/spec/validations_spec.rb b/spec/validations_spec.rb index 6310911..bb1666f 100644 --- a/spec/validations_spec.rb +++ b/spec/validations_spec.rb @@ -1,4 +1,4 @@ -require File.dirname(__FILE__) + '/spec_helper' +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe ValidatesTimeliness::Validations do attr_accessor :person