Ruby on Rails: Nested Attributes, belongs_to relation
Asked Answered
C

4

9

I have a User entity that has a Current Location field (city and country). To hold this info I created an entity called Location which has_many Users.

I'm not entirely sure if I should put in the User model "has_one" or "belongs_to", but for what I read if I wanted it to have the foreign key of the location I should put "belongs_to". I also want to be able to edit the user's Current Location when editing the User. so I am using nested attributes. But when I edit the user I end up adding a new Location each time without ever associating it to the user that was edited. Can you help me out?

My code is the following:

#User Model
class User < ActiveRecord::Base
  ## Relationships
  belongs_to :current_location, :class_name => 'Location'
  accepts_nested_attributes_for :current_location
end

#Location Model
class Location < ActiveRecord::Base
  #Relationship
  has_many :users
end

# part of the _form_edit.haml
- form_edit.fields_for :current_location do |location_form|
  = location_form.label :location, "Current Location"
  = location_form.text_field :location

#Application Helper
#nested attributes for user and location
def setup_user(user)
  returning(user) do |u|
    u.build_current_location if u.current_location.nil?
  end
end

#in the user controller (added after edit)
def update
    @user = @current_user
    if @user.update_attributes(params[:user])
      flash[:notice] = "Account updated!"
      redirect_to account_url
    else
      render :action => :edit
    end
  end
Coulson answered 20/10, 2009 at 11:6 Comment(2)
And in the controller that saves the datas, what do you have ?Instanter
I have: def update @user = @current_user if @user.update_attributes(params[:user]) flash[:notice] = "Account updated!" redirect_to account_url else render :action => :edit end endCoulson
T
11

The exact problem you're facing, as others have pointed out is that your controller is not receiving the location id as it should. Looks to me the location id is being passed through the wrong parameter. Unfortunately a location id doesn't exist on a new record, so this is not possible in the form.

Your problem stems from the use accepts_nested_attributes_for on a belongs_to relationship. The behaviour isn't clearly defined. This appears to be a documented bug. So the accepts_nested_attributes_for should be on a has one or has many side of a relationship.

Here are some possible solutions:

  1. Move The accepted_nested_attributes_for to the Location model and build your forms the other way around.

    -form_for @location do |location_form|
     ...
     =location_form.fields_for @user do |user_form|
       ....
    

    Unfortunately this doesn't allow for a logical way of presenting information. And makes editing the right user difficult.

  2. Use a join model, and make a has one :through relationship.

    I'm honestly not sure how well accept_nested_attributes_for works with a :through relationship, but it will definitely solve your problem with linking records.

  3. Ignore accepts_nested_attributes_for and handle the association in your controller the old fashioned way.

    Actually keep the accepts_nested_attributes_for. It provides some handy convenience methods, just don't let it get to the update_attributes/create statement.

    def update 
      @user = @current_user 
      completed = false
      location_params = params[:user].delete(:current_location_attributes)
    
      User.transaction do
        @location = Location.find_or_create_by_id(location_params)
        @user.update_attributes(params[:user]) 
        @user.current_location = @location
        @user.save!
        completed = true
      end
      if completed
        flash[:notice] = "Account updated!" redirect_to account_url 
      else 
        render :action => :edit 
      end
    end
    

Fields for will populate an id field in the current_location_attributes hash automatically, if it's not creating a new location. However, find_or_create_by_id, requires an :id entry in the hash for it to work. It will create with a correctly auto incremented id if the id isn't in the database. If you are creating a new location you will need to add it. Easiest to add it to the form with =location_form.hidden_field :id, 0 unless current\_location.new\_record?.

However, you might want to cut down on duplicate location creation, and change the Location.find_or_create_by_id line to Location.find_or_create_by_location. This will also cut down on any errors from failed uniqueness validations.

Tolerable answered 20/10, 2009 at 16:10 Comment(7)
hi EmFI tks for your reply. I didn't know it was a known bug... but I really tried to search for a solution and haven't found anything. Regarding the 3 options. 1- Is indeed not the best, for this purpose.. =/ Because I want to change a user's Location. 2- I don't know how to do this to say the truth... 3- This seemed quite nice and I tried it but it doesn't work. =/ At first it said that there was no "delete!" function, but after removing the "!" it didn't have any errors. It just doesn't associate the location with the user :s It just creates it and that's all... :s Any ideas? :/Coulson
Yeah, I assumed there was a destructive delete on Hash. Also in my haste, I forgot to add the line that actually associates the user with the location. I've corrected the mistake.Tolerable
I don't know what's wrong, but unfortunately it doesn't seem to do the trick :( sorry :sCoulson
Are you getting any errors. Exactly what parameters are the controller receiving?Tolerable
for what I see it is sending this: "current_location_attributes"=>{"location"=>"Porto, Portugal"}, Not sure if it should have the :id of the location... how to do that? I don't get any error now.Coulson
Seems I glossed over another detail: My code was removing location_attributes, instead of current_location_attributes from the params hash. Updated for correctness.Tolerable
Hi there, I think that in the hidden field it should be @user.current_location.new_record? right? Otherwise I get an error for non-existing variable or method with the name "current_location". Nevertheless it still doesn't send any id nor it works :( I'm going nuts with this. If this ends up not working I'll just move the current_location to the user. :sCoulson
I
0

You do not provide the nested attribute's id. So rails thinks it's a new one.

- form_edit.fields_for :current_location do |location_form|
    = location_form.label :location, "Current Location"
    = location_form.text_field :location
    = location_form.hidden_field :id unless location_form.new_record?
Instanter answered 20/10, 2009 at 11:12 Comment(1)
Hi there. Thank you for your fast reply. I've added that field but the same thing continues to happen. It adds a new Location to the table. And it doesn't make the association for the User. So the show.haml never shows the current_location :( it's always empty.Coulson
M
0

Not sure if the previous answer is really correct. What you need is to specify the id of the user for the location, not the location itself.

- form_edit.fields_for :current_location do |location_form|
  = location_form.label :location, "Current Location"
  = location_form.text_field :location
  = location_form.hidden_field :user_id
Mallorca answered 20/10, 2009 at 12:51 Comment(3)
Hi, thanks for your reply. When I add that hidden field I get the following error message: "undefined method `user_id' for #<Location:0x54982c0>" . Shouldn't he have it because of the "has_many :users" relationship?Coulson
oh, sorry, confused something in your domain model. You should try form_edit.hidden_field :location_idMallorca
Hi again. tried again the hidden_field with :location_id and :current_location_id ... but nevertheless it continues the same :s not working as it should =(Coulson
C
0

By default belongs_to :current_location, :class_name => 'Location' will expect the Users table have a current_location_id field. Once you have this you should be able to do something like:

@user = @current_user
@user.update_attributes(params[:user])

@location = @user.current_location or @user.build_current_location
@location.update_attributes(params[:location]) 

@user.current_location.save!
@user.save!
Caracalla answered 20/10, 2009 at 19:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.