Question

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
Was it helpful?

Solution

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.

OTHER TIPS

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?

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

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!
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top