3

Say I have a custom setter on an activemodel model DateRange, to automatically set a DateTime attribute if a string is entered. It looks like this

  def from=(value)
    @from = value.to_date
  end

Now this does work if you enter a valid date string, but if you enter an invalid date, it stalls my application. How can I cancel assignment so that the application will move on and the DateRange fails validation later on in my controller?

Marco Prins
  • 7,189
  • 11
  • 41
  • 76

3 Answers3

3
def from=(value)
  begin
    @from = value.to_date
  rescue ArgumentError, NoMethodError
    errors.add(:from, "is not a valid date")
  end
end

Probably a better way to do it but that should work

j-dexx
  • 10,286
  • 3
  • 23
  • 36
  • 1
    Problem with this is that doesn't prevent the attribute from being saved (with a `from` attribute as it was previously). For that I've found you need to encapsulate this logic inside a validator: http://stackoverflow.com/a/9676246/1755300 – unmultimedio Nov 29 '16 at 20:50
  • 2
    This solution is NOT correct. Calling `valid?` would completely ignore the added error and the model can be created/updated. – Itay Grudev May 31 '17 at 10:32
  • 1
    This will not work because `valid?` will [clean](https://github.com/rails/rails/blob/v6.0.3.2/activemodel/lib/active_model/validations.rb#L336) `errors` before performing actual validation callbacks – Kirill Aug 19 '20 at 12:57
2

You would generally want to rescue/handle any problem in your setter.

def from=(value)
  @from = Date.parse(value)
rescue ArgumentError
  nil
end

Then let your normal date validations handle the nil value later.

I wouldn't add to your errors in the setter as it should not need to know about your validations and vice versa.

jordelver
  • 8,292
  • 2
  • 32
  • 40
  • Wouldn't that be confusing to a user though as it would say it can't be blank (assuming validating presence) even though they input something? – j-dexx Mar 04 '14 at 13:20
1

As @jordelver says, there is no point in adding validation errors except during validation. However, you can save the error in a virtual attribute and check for it during validation.

validate :no_bad_dates
attr_accessor :bad_date_error

def from=(value)
  @from = Date.parse(value)
rescue ArgumentError => ex
  self.bad_date_error = ex
end

def no_bad_dates()
  errors.add(:from, bad_date_error.message) if bad_date_error
end
Tom Wilson
  • 797
  • 9
  • 26
  • Note that `bad_date_error` does not need to be publicly accessible. Implemented here as `attr_accessor` to simplify the story. – Tom Wilson Feb 20 '18 at 18:45