Dudo
Dudo

Reputation: 4169

Rails ActiveRecord callbacks

I'm having an issue with a date format. I have a time picker that has the date in a funky format (well, it's a nice format, actually, but not to the computer). I'm trying to have Chronic parse the date so that it can be saved properly.

At first, I was doing, in the create action of my controller:

params[:event][:start] = Chronic.parse(params[:event][:start])

but if and when validation fails, it sends the parsed value back to the view, and my datetimepicker is all botched, then.

So, I thought... callback? In my model, I added:

private
  def date_change
    self.start = Chronic.parse(self.start)
  end

I tried before_save, before_validation, after_validation... but nothing seems to get that date formatted correctly.

As it stands, I keep getting ArgumentError in EventsController#create - Argument out of range. I assume that's because the database is expecting a properly formatted datetime object.

Any idea on how I can accomplish my goal, here, of not changing the params, but still being able to save a properly formatted object?

Upvotes: 2

Views: 423

Answers (2)

nathanvda
nathanvda

Reputation: 50057

Assumption is the mother of all mess ups :)

  • are you sure the callback is hit? Because if it would, and the error occurred (like it did), wouldn't it still send back the incorrect data (because parsed) back to the view? In case of doubt: log something to make sure it is hit.
  • are you sure which field causes the Argument out of range error.

Most cases bugs are so hard to find/fix because we assume we know the error, but we are looking at the error in the wrong way.

Easy ways to test which attribute causes the error:

  • open rails console, build an object with the parameters, save it, and ask the errors. Something like

    e = Event.new(params[:event]) # copy params verbatim from your logfile e.save e.errors

and that will display which field causes the error.

Alternatively: use pry and add a line binding.pry just after the save, so you inspect the errors (more info)

Answer (assuming your assumption was correct)

I see two options to do what you want:

  • use the after_validation callback, if you are sure the data will always be correct, and correctly parsed by Chronic. This way if validation is passed, then convert the field and normally nothing can go wrong anymore, and the value is never sent to the browser again.

Note: if some other attribute is causing the error, this callback is never hit, of course. Because it does not pass the validation.

  • use a virtual attribute, e.g. start_str, which is a visual representation of your start, and before_save convert it to start. It does not really matter that much here, because if validation fails, you just show start_str and not the "real" start field.

Upvotes: 0

mu is too short
mu is too short

Reputation: 434585

I'm guessing that the problem is occurring the the start= mutator method that ActiveRecord supplies. If you're doing things like this in your controller:

@event.update_attributes(params[:events])
@event = Event.create(params[:event])
#...

then create and update_attributes should call start= internally. That should allow you to put the Chronic stuff in your own start=:

def start=(t)
  super(Chronic.parse(t))
end

You might need to adjust that for non-String ts, I'm not sure what Chronic.parse(Time.now), for example, would do. You could also call write_attribute(:start, Chronic.parse(t)) or self[:start] = Chronic.parse(t) if you didn't want to punt to super.

Note that before_validation and similar handlers will be called too late to bypass whatever default string-to-timestamp conversion ActiveRecord is doing but a mutator override should happen at the right time.

Alternatively, you could parse the time in the controller with something like this:

event = params[:events].dup
events[:start] = Chronic.parse(events[:start])
@event = Event.create(event)

Upvotes: 2

Related Questions