Skip to content

Instantly share code, notes, and snippets.

@johnathanludwig
Last active September 8, 2015 21:26
Show Gist options
  • Save johnathanludwig/9983091 to your computer and use it in GitHub Desktop.
Save johnathanludwig/9983091 to your computer and use it in GitHub Desktop.
A custom validator for date ranges. Read the comments for directions on how to use it. Place this file in your initializers directory. I prefer this structure: config/initializers/extensions/active_model/validations/date_range.rb
module ActiveModel
module Validations
class DateRangeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
pass = true
if comparison_date = options[:before]
if record.send(comparison_date).present?
pass = value < record.send(comparison_date)
expected = 'before'
end
elsif comparison_date = options[:on_or_before]
if record.send(comparison_date).present?
pass = value <= record.send(comparison_date)
expected = 'on or before'
end
elsif comparison_date = options[:after]
if record.send(comparison_date).present?
pass = value > record.send(comparison_date)
expected = 'after'
end
elsif comparison_date = options[:on_or_after]
if record.send(comparison_date).present?
pass = value >= record.send(comparison_date)
expected = 'on or after'
end
end
record.errors[attribute] << (options[:message] || "must be #{expected} #{comparison_date.to_s.gsub('_', ' ')}") unless pass
end
end
module HelperMethods
# Validates that the specified date is valid based on the comparison date.
#
# class Person < ActiveRecord::Base
# validates_date_range_of :start_date, before: :end_date
# end
#
# In this case, the start_date must be before the end_date.
# If either date is not present, no error will be added.
# Use <tt>validates_presence_of</tt> if you need to require them.
#
# Other options are:
# validates_date_range_of :start_date, on_or_before: :end_date
# validates_date_range_of :end_date, after: :start_date
# validates_date_range_of :end_date, on_or_after: :start_date
def validates_date_range_of(*attr_names)
validates_with DateRangeValidator, _merge_attributes(attr_names)
end
end
end
end
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
# we use the shoulda gem for testing.
class TestClass
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :start_date, :end_date
def initialize(attributes={})
attributes && attributes.each do |name, value|
send("#{name}=", value) if respond_to? name.to_sym
end
end
end
class DateRangeTest < ActiveSupport::TestCase
context 'DateRange' do
should 'not add error if main field is null' do
TestClass.validates_date_range_of :start_date, before: :end_date
t = TestClass.new(start_date: nil, end_date: Date.yesterday)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be before end date/
end
context 'validates before' do
should 'not add error if comparison field is null' do
TestClass.validates_date_range_of :start_date, before: :end_date
t = TestClass.new(start_date: Date.yesterday, end_date: nil)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be before end date/
end
should 'not add error if value is before comparison field' do
TestClass.validates_date_range_of :start_date, before: :end_date
t = TestClass.new(start_date: Date.yesterday, end_date: Date.today)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be before end date/
end
should 'add error if end_date is before start date' do
TestClass.validates_date_range_of :start_date, before: :end_date
t = TestClass.new(start_date: Date.today, end_date: Date.yesterday)
t.valid?
assert_contains t.errors.full_messages, /must be before end date/
end
end
context 'validates on_or_before' do
should 'not add error if comparison field is null' do
TestClass.validates_date_range_of :start_date, on_or_before: :end_date
t = TestClass.new(start_date: Date.yesterday, end_date: nil)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be on or before end date/
end
should 'not add error if value is on same day as comparison field' do
TestClass.validates_date_range_of :start_date, on_or_before: :end_date
t = TestClass.new(start_date: Date.today, end_date: Date.today)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be on or before end date/
end
should 'add error if end_date is before start date' do
TestClass.validates_date_range_of :start_date, on_or_before: :end_date
t = TestClass.new(start_date: Date.today, end_date: Date.yesterday)
t.valid?
assert_contains t.errors.full_messages, /must be on or before end date/
end
end
context 'validates after' do
should 'not add error if comparison field is null' do
TestClass.validates_date_range_of :end_date, after: :start_date
t = TestClass.new(start_date: Date.yesterday, end_date: nil)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be after start date/
end
should 'not add error if value is after comparison field' do
TestClass.validates_date_range_of :end_date, after: :start_date
t = TestClass.new(start_date: Date.yesterday, end_date: Date.today)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be after start date/
end
should 'add error if end_date is after start date' do
TestClass.validates_date_range_of :end_date, after: :start_date
t = TestClass.new(start_date: Date.today, end_date: Date.yesterday)
t.valid?
assert_contains t.errors.full_messages, /must be after start date/
end
end
context 'validates on_or_after' do
should 'not add error if comparison field is null' do
TestClass.validates_date_range_of :end_date, on_or_after: :start_date
t = TestClass.new(start_date: Date.yesterday, end_date: nil)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be on or after start date/
end
should 'not add error if value is on same day as comparison field' do
TestClass.validates_date_range_of :end_date, on_or_after: :start_date
t = TestClass.new(start_date: Date.today, end_date: Date.today)
t.valid?
assert_does_not_contain t.errors.full_messages, /must be on or after start date/
end
should 'add error if end_date is after start date' do
TestClass.validates_date_range_of :end_date, on_or_after: :start_date
t = TestClass.new(start_date: Date.today, end_date: Date.yesterday)
t.valid?
assert_contains t.errors.full_messages, /must be on or after start date/
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment