views:

131

answers:

2

I have several models with a date attribute and for each model I'd like to validate these dates against a given range. A basic example would be:

validates_inclusion_of  :dated_on, :in => Date.new(2000,1,1)..Date(2020,1,1)

Ideally I'd like to evaluate the date range at runtime, using a similar approach as named_scope uses, e.g:

validates_inclusion_of  :dated_on, :in => lambda {{ (Date.today - 2.years)..(Date.today + 2.years)}}

The above doesn't work of course, so what is the best way of achieving the same result?

+2  A: 

If the validation is the same for each class, the answer is fairly simple: put a validation method in a module and mix it in to each model, then use validate to add the validation:

# in lib/validates_dated_on_around_now
module ValidatesDatedOnAroundNow
  protected

  def validate_dated_around_now
    # make sure dated_on isn't more than five years in the past or future
    self.errors.add(:dated_on, "is not valid") unless ((5.years.ago)..(5.years.from_now)).include?(self.dated_on)
  end
end

class FirstModel
  include ValidatesDatedOnAroundNow
  validate :validate_dated_around_now
end

class SecondModel
  include ValidatesDatedOnAroundNow
  validate :validate_dated_around_now
end

If you want different ranges for each model, you probably want something more like this:

module ValidatesDateOnWithin
  def validates_dated_on_within(&range_lambda)
    validates_each :dated_on do |record, attr, value|
      range = range_lambda.call
      record.errors.add(attr_name, :inclusion, :value => value) unless range.include?(value)
    end
  end
end

class FirstModel
  extend ValidatesDatedOnWithin
  validates_dated_on_within { ((5.years.ago)..(5.years.from_now)) }
end

class SecondModel
  extend ValidatesDatedOnWithin
  validates_dated_on_within { ((2.years.ago)..(2.years.from_now)) }
end
James A. Rosen
Thanks, that's helpful but I've just edited my question to improve the explanation. I'm ideally looking for a more generic solution which I can use across models and for any given date field.
Olly
I updated this solution to match the updated use case more closely, but I far prefer my other solution.
James A. Rosen
+1  A: 

A different solution is to rely on the fact that validates_inclusion_of only requires an :in object that responds to include?. Build a delayed-evaluated range as follows:

class DelayedEvalRange
  def initialize(&range_block)
    @range_block = range_block
  end
  def include?(x)
    @range_block.call.include?(x)
  end
end

class FirstModel
  validates_inclusion_of :dated_on, :in => (DelayedEvalRange.new() { ((5.years.ago)..(5.years.from_now)) })
end
James A. Rosen