Search code examples
ruby-on-railsactiverecordnested-formshas-one-through

Rails has_one, belongs_to, join_table and nested form


I'm creating a User, Role and UserRole. A user can create a list of roles, and from the new user form, there is a nested form which populate a list of roles created, then the user able to select a role and associated with the new user. I'm able to create a list of roles, but facing problem when creating a nested form in the new user view file.

Here are the models, kindly advise me if relationships are correct.

class User < ApplicationRecord
  has_one :user_role
  has_one :role, through: :user_role
end

class Role < ApplicationRecord
  has_many :user_roles
  has_many :users, through: :user_roles
end

class UserRole < ApplicationRecord
  belongs_to :user
  belongs_to :role
end

User controller. I'm not sure if my controller is correct:

def new
  @user = User.find_by_id(params[:id])
  @user = @current_user.account.users.new
  @user.build_user_role
end

def create
  @user = User.find_by_id(params[:id])                
  @user = @current_user.account.users.create_with_password(user_params)
    if @user.save
       redirect_to users_path
    else
       render 'new'
    end
end

private
def user_params
  params.require(:user).permit(:id, :email, :password, :password_confirmation, :admin, :owner, user_role_attributes: [:user_id, :role_id])
end

end

Below is new user form:

<%= form_for(@user, remote: true) do |f| %>

<%= f.text_field :email, class: "form-control", autofocus: true, autocomplete: "off" %>
<%= f.check_box :admin, class:"checkbox" %>
<%= f.check_box :owner, class:"checkbox" %>

<%= f.fields_for :user_role do |ff| %>
<%= ff.collection_select :role_id, @roles, :id, :role_name, include_blank: false %>
<% end %>             

<%= f.button "Create",  class: "btn btn-success" %>

<% end %>

The nested form for user_role doesn't show up, and also kindly advise if the relationships between User, Role and UserRole are correct.


Solution

  • If the user only can ever have one role you don't really need the user_roles join table in the first place:

    class User < ApplicationRecord
      belongs_to :role
    end
    
    class Role < ApplicationRecord
      has_many :users
    end
    

    Although it seems pretty naive to assume that you won't actually want a many to many association which is actually a lot more useful. Eg:

    class User < ApplicationRecord
      has_many :user_roles
      has_many :roles, through: :user_roles
    end
    
    class Role < ApplicationRecord
      has_many :user_roles
      has_many :users, through: :user_roles
    end
    
    class UserRole < ApplicationRecord
      belongs_to :user
      belongs_to :role
    end
    

    You're also following into a common beginner trap where you confuse nested attributes with simply selecting an association.

    You don't need @user.build_user_role or f.fields_for :user_role. You just need a select and to whitelist the role_id param:

    <%= form_for(@user, remote: true) do |f| %>
      # ...
      <%= f.collection_select :role_id, @roles, :id, :role_name, include_blank: false %>
      # ...
    <% end %>
    

    def new
      # why even do this if you are just reassigning it on the next line?
      @user = User.find_by_id(params[:id]) 
      @user = @current_user.account.users.new
    end
    
    def create
      @user = User.find_by_id(params[:id])
      # Violation of Law of Demeter - refactor!
      @user = @current_user.account.users.create_with_password(user_params)
      if @user.save
        redirect_to users_path
      else
        render 'new'
      end
    end
    
    private
    def user_params
      params.require(:user)
            .permit(
              :id, :email, :password, 
              :password_confirmation, 
              :admin, :owner, 
              :role_id # this is all you really need
            )
    end