mirror of
https://github.com/ditkrg/validates_timeliness.git
synced 2026-01-22 22:06:45 +00:00
173 lines
7.0 KiB
Ruby
173 lines
7.0 KiB
Ruby
module ValidatesTimeliness
|
|
# Adds ActiveRecord validation methods for date, time and datetime validation.
|
|
# The validity of values can be restricted to be before and/or certain dates
|
|
# or times.
|
|
module Validations
|
|
|
|
# Error messages 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
|
|
|
|
error_messages = {
|
|
:invalid_datetime => "is not a valid %s",
|
|
: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"
|
|
}
|
|
ActiveRecord::Errors.default_error_messages.update(error_messages)
|
|
end
|
|
|
|
module ClassMethods
|
|
|
|
# Override this method to use any date parsing algorithm you like such as
|
|
# Chronic. Just return nil for an invalid value and a Time object for a
|
|
# valid parsed value.
|
|
def timeliness_date_time_parse(raw_value, type, strict=true)
|
|
return nil if raw_value.blank?
|
|
return raw_value.to_time 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.new(*time_array[0..2]) unless type == :time
|
|
|
|
# Create time object which checks time part, and return time object
|
|
make_time(time_array)
|
|
rescue
|
|
nil
|
|
end
|
|
alias_method :parse_date_time, :timeliness_date_time_parse
|
|
|
|
# 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 = timeliness_date_time_parse(raw_value, configuration[:type])
|
|
record.send("#{attr_name}=", nil)
|
|
record.errors.add(attr_name, configuration[:invalid_datetime_message] % configuration[:type])
|
|
next
|
|
end
|
|
|
|
validate_timeliness_restrictions(record, attr_name, time, configuration)
|
|
rescue Exception => e
|
|
record.send("#{attr_name}=", nil)
|
|
record.errors.add(attr_name, configuration[:invalid_datetime_message] % configuration[:type])
|
|
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
|
|
|
|
# Validate value against the temopral restrictions. Restriction values
|
|
# maybe of mixed type, so the 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 => '>='}
|
|
|
|
conversion_method = case configuration[:type]
|
|
when :time then :to_dummy_time
|
|
when :date then :to_date
|
|
when :datetime then :to_time
|
|
end
|
|
|
|
value = value.send(conversion_method)
|
|
|
|
restriction_methods.each do |option, method|
|
|
next unless restriction = configuration[option]
|
|
begin
|
|
compare = case restriction
|
|
when Time, Date, DateTime
|
|
restriction
|
|
when Symbol
|
|
record.send(restriction)
|
|
when Proc
|
|
restriction.call(record)
|
|
else
|
|
parse_date_time(restriction, configuration[:type], false)
|
|
end
|
|
|
|
next if compare.nil?
|
|
|
|
compare = compare.send(conversion_method)
|
|
record.errors.add(attr_name, configuration["#{option}_message".to_sym] % compare) 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_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
|