RyanJM
RyanJM

Reputation: 7068

*_attributes=() fails validation for nested attribute

Original title: before_validation on update - not handling error properly

I'm trying to figure out how to handle the validation of a nested object, especially on the update.

My parent class that handles the nested attributes.

meeting.rb

has_many :proposed_times, :dependent => :destroy, :inverse_of => :meeting
validates_associated :proposed_times
accepts_nested_attributes_for :proposed_times
...
def proposed_times_attributes=(attributes)
  attributes.each do |key,value|
    value[:timezone] = timezone
    if value[:id]
      p = ProposedTime.find(value[:id])
      value.delete(:id)
      p.update_attributes(value)
    else
      self.proposed_times << ProposedTime.new(value)
    end
  end
end

The class I'm trying to validate is below. Basically I need to validate that the date is in the future. And I'm constructing my starting_at with a before_validation method. So I'm not sure how to handle the case that (A) it can't properly construct the date (in the case where the user deletes the date I need to tell the user to fill it in) (B) If I do construct it, then it fails the save, I don't know how to get it to go back to the view to correct the information.

proposed_times.rb

belongs_to :meeting, :inverse_of => :proposed_times
validate :future_enough
before_validation :construct_starting_at
...
def future_enough
  unless starting_at && starting_at >= (Time.now + 15.minutes)
    errors.add(:not_far_enough_out, "msg not used but: starting_at isn't far enough out")
  end
end

def construct_starting_at
  if date.present? && time.present? && timezone.present?
    begin
      d = Time.parse(date)
      self.starting_at = Time.zone.parse("#{d.year}-#{d.month}-#{d.day} #{time.hour}:#{time.min}:00 #{timezone}")
    rescue
      self.starting_at = nil
    end
  end
end

Here is the view I'm using. I know this isn't the best way to handle this functionality, but I'm working with what I have… Everything I do redirects to thank_you. I'm not sure why it isn't failing on the save.

meeting_controller.rb

def propose_times
  @consultation = Consultation.find(params[:id])
  @user = current_user

  # If this is coming second time around with data to save
  if request.put?
    @consultation.attributes = params[:consultation]
    if @consultation.save
      redirect_to thank_you_path
    else
      render :layout => "simple"
    end
  else
    render :layout => "simple"
  end
end

What is the proper way for me to validate a nested object and handle the errors? Do I need to be using new_record? for the before_validation method?

Update: So using @Aditya's suggestion of validates_associated, it still doesn't validate just the updated data. The updated times I put in get validated… but since they aren't valid, they aren't kept, and then it gets validated again (with the old data) and then that passes/fails validation and the warnings for those dates are shown.

What am I missing?

Update 2: I added a bunch of p statements to see the order of events and if I leave out the validated_associated, then it only gets validated once, but for some reason the meeting itself saves fine and therefore it redirects to thank you. But if I leave it in, it validates twice, hence the issues. So why is it validating the first (or second) time and how do I stop that?

Update 3: Ok, so I took out my proposed_times_attributes= function and in the view added the timezone there. This fixed the issue (along with @Aditya's method). I wish I knew how to get it to work with a defined method like that. I tried various other combinations of having/not having validates_associated, accepts_nested_attributes_for, and having that function defined.

SO if you come across this, and happen to know how to write proposed_times_attributes= so the validation would still work, please let me know.I need to be able to put timezone in proposed_times_attributes= because on view I would like to put one timezone select box for all of the proposed_times. I wish there was a way to call super() inside the *_attributes=() method.

Upvotes: 1

Views: 1101

Answers (2)

RyanJM
RyanJM

Reputation: 7068

Finally figured it out. So you can do a super()-type method by calling assign_nested_attributes_for_collection_association or assign_nested_attributes_for_one_to_one_association. But you need to also call accepts_nested_attributes_for in order to set some of the options for those methods (click on the function's name to see the documentation for each).

accepts_nested_attributes_for :advisor, :client, :proposed_times
...
def proposed_times_attributes=(attributes)
  attributes.each do |key,value|
    value[:timezone] = client.timezone
  end
  assign_nested_attributes_for_collection_association(:proposed_times, attributes)
end

Upvotes: 1

Aditya Sanghi
Aditya Sanghi

Reputation: 13433

@RyanJM

You don't need to manually validate the child associate entities if you use the following

meeting.rb

validates_associated :proposed_times

assuming you already have the following association in your meeting.rb

has_many :proposed_times 

Upvotes: 0

Related Questions