I have recently been tinkering with ruby on rails again and was banging my head against the wall with a multiple select list for a “has and belongs to many” relationship between models. After a lot of Googling and experimenting I finally got it running. The resulting code is deceptively simple!
This example illustrates the relationship between users and their roles. Each user can have multiple roles, and each role can have multiple users.
The User Model
# models/admin/user.rb class Admin::User < ActiveRecord::Base has_one :division, :class_name => "Admin::Division" has_and_belongs_to_many :role, :class_name => "Admin::Role" validates_presence_of :first_name, :last_name, :user_name, :password end
The Role Model
# models/admin/role.rb class Admin::Role < ActiveRecord::Base has_and_belongs_to_many :user, :class_name => "Admin::User" validates_presence_of :role end
The User View (partial)
# views/admin/users/_form.html.erb
<% form_for(@user) do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :first_name %><br />
<%= f.text_field :first_name %>
</p>
<p>
<%= f.label :last_name %><br />
<%= f.text_field :last_name %>
</p>
<p>
<%= f.label :user_name %><br />
<%= f.text_field :user_name %>
</p>
<p>
<%= f.label :password %><br />
<%= f.password_field :password %>
</p>
<p>
<%= f.label :role %><br />
<%= collection_select(:admin_user, :role_ids, @roles, :id, :role,
{ :selected => @user.role_ids },
{:multiple => true, :role => 'admin_user[role_ids][]' }) %>
</p>
<p>
<%= f.submit 'Save' %>
</p>
<% end %>
The User Controller
# excerpt from controllers/admin/users_controller.rb
# This is just the update method
#
def update
# if the multiple select list is empty,
# make sure the role_ids array exists
params[:admin_user][:role_ids] ||= []
respond_to do |format|
if @user.update_attributes(params[:admin_user])
flash[:notice] = 'User was successfully updated.'
format.html { redirect_to(@user) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
end
end
end
Display Comma Delimited Roles
Once I got the select list working, I wanted a simple way to display a comma delimited list of a user’s roles. I messed about with looping over all roles and just displaying the ones that matched the appropriate values, but it turns out there is a much simpler and elegant way to do this. The following snippet of code leverages the HMABT mapping between the user model and role model to access all the role names associated with a particular user. It then joins them all together delimiting the values with commas.
# excerpt from views/admin/users/index.html.erb
#
<%=h user.role.map(&:role).join(', ') %>
The Admin Name Space
Please note that my example is complicated a little by the fact that the models, views and controllers are all within a “/admin” name space. Here’s an excerpt from my routes.rb file which shows the necessary code to make it work.
# excerpt from config/routes.rb
map.namespace :admin do |admin|
admin.resources :roles, :users
end
Additional Resources
The following resources were quite helpful to me in getting this working:
Thank you for the helpful article. The “role_ids” bit solved my problem (“Expected Object got String” errors.)