Has and Belongs to Many with Checkboxes
Here’s how to update/create habtm (has_and_belongs_to_many) relationships with checkboxes.
app/model/user.rb:
<pre>
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
</pre>
app/controllers/users_controller.rb:
<pre>
class UsersController < ApplicationController
# There's nothing special about this update method from the
# standard scaffold generated update method. All the "magic"
# is in the automatically generated group_ids= method in the
# model called by update_attributes.
def update
@user = User.find(@params['id'])
user.update_attributes(params‘user’)
flash‘notice’ = “Update Successful”
else
render_action ‘edit’
end
end
end
app/views/users/form.html:
<pre>
<ul>
<% Group.find(:all).each do |g| %>
<li><input type="checkbox" name="user[groupids][]"
value=“<%= g.id %>”
<% if @user.groups.include?(g) > checked=“checked” < end > />
<= g.name %>
<% end %>
alternatively, the checkbox can be represented by a helper:
<pre>
<ul>
<% Group.find(:all).each do |g| %>
<li><%= check_box_tag("user[group_ids][]",
g.id,
@user.groups.include?(g)) %>
<%= g.name %>
<% end %>
What this view does is dynamically generate a checkbox for every group. The name of the input is significant obviously. The trailing “[]” on the name means the end result will be the list of checked ids. This list will be stored on the @params['user'] hash with the key ‘groups_ids’. When the controller calls @user.update_attributes(params‘user’)@, it tries to call @user.key= for each of the keys on @params['user']. What’s important to realize is that these keys don’t have to actually be attributes on the User model. All that’s important is that there’s a key= method on the model. Since Rails 0.13, the model automatically contains a “collection_ids=” method for habtm and has-many associations.
This method will load the objects identified by the ids and call the “group=(list)” method on the model with the freshly loaded list. This method in turn, will compare the list to the current list of groups and delete/add groups as necessary.
skanthak (2005-07-24): This has been updated for Rails 0.13. Is it working for people now?
This was originally posted in MultipleSelectOptionsHelper.
Then discussed on the rails listserv
And now compiled here.
I tried this to the letter but kept getting this error.
ActiveRecord::AssociationTypeMismatch in Users#update
Group expected, got Array
Instead i tried the method here and it works correctly. looks like the id in the array is not getting converted to the type of the array element, in this case Group.
http://www.lathi.net/twiki-bin/view/Main/HabtmCheckbox?skin=print
my system is:
rails 0.13.1
win32
There was a syntax error in has_group? ( missing end ).
fixed in case anyone was having a problem.
Works for me! Thanks -JamesM
I had to add this code above the update_attributes call in the controller’s update method for unchecking all groups to actually clear the group records:
<pre>
class UsersController < ApplicationController
def update
@user = User.find(@params['id'])
user.update_attributes(params‘user’)
flash‘notice’ = “Update Successful”
else
render_action ‘edit’
end
end
end
When update_attributes is called, it will only call the group_ids= function if params['user']['group_ids'] exists. Can someone confirm this before I merge this code into the controller’s update method above? – JesseNewland
Jesse is right. If all the checkboxes are unchecked (i.e. if you want to remove a user from all the groups) the group_ids= fuction is never called and thus the changes are not updated.
Javier Smaldone
So where is the group stuff? how would you store permissions for a group and a user using one table for storing the actual permissions and foreign keys?
Thanks for this tutorial, it was sooo easy. I’m truly amazed. :) – Ian Neubert
Here’s a little helper function….
def collection_check_boxes object, method, collection, value_method, text_method
collection.collect do |c|
check_box_tag("#{object}[#{method.singularize}_ids][]", c.send(value_method), eval("@#{object}").send(method).include?(c)) + " #{c.send(text_method)}"
end.join ' '
end
This doesn’t seem to work for inserting new records, only for updating existing records. When I create a new item and try setting the foo_ids where foo is a many-to-many, it doesn’t get saved to the database. When I update a record that already exists in the database, it works (the join records are created). To get it to work, I need to re-fetch the newly create row by its id, then call update_attributes on it.
def create
department = Department.new(params[:department]) # FIX! can't set many-to-many relationships on creation if @department.save Department.find(department.id).update_attributes(params[:department]) # hack to get around many-to-many not saving until object has an id
flash[:notice] = ‘Department was successfully created.’
redirect_to :action => ‘list’
else
render :action => ‘new’
end
end
Submitted this as a bug: http://dev.rubyonrails.org/ticket/3692
Sam Barnum?
I noticed the same thing after upgrading to Rails 1.0. I was on 0.13.1 and the habtm checkboxes worked flawlessly using the techniques described above. Now I have had to add the hack that Sam describes in order to get the join ids to save on ‘create’. Anyone else having this problem?
I had the same problem and the hack fixed it.
This is probably a new bug, this broke with 1.0.
def groups_ids=(list)
groups.clear
groups << Group.find(list)
end
Here is my workaround to Sam’s problem. It seems that this is not a problem with IDs at all. The way I worked around it would indicate that it is a bug in the validation code.
#workaround for bug #3692
@@user_ids = nil
def before_validation_on_create
unless self.users.empty?
@@user_ids = self.users.collect { |user| user.id }
self.users.clear
end
end
def after_validation_on_create
unless @@user_ids == nil
self.user_ids = @@user_ids
@@user_ids = nil
end
end
You may want to consider using update_attribute rather then update_attributes so that you avoid validating the original object when saving since it’s not being changed. In my case I’m using a seperate form to assign the related object to the user so the only information in the params is an array of the :group_ids rather than a complete user object + group_ids. In this situation you can fail some types of validation resulting it update_attributes returning false.
To avoid this the update function should look like:
def update_groups
@user = User.find(@params['id'])
if !params[:user]
@user.groups.clear
else
if @user.update_attribute(:group_ids,@params['user']['group_ids'])
flash['notice'] = "Update Successful"
else
render_action 'edit'
end
end
end
I had to do this in my app since update_attributes was always returning false because my user model has a validates_confirmation_of :password validation in it. – Kevin Kolk
Note that if you use a table named ‘attribute’ you will be forced to deal with a name space issue -PeterLD
(Rolled back to unspammed Version — Phillip)x2
(And again – Láďa)
Has and Belongs to Many with Checkboxes
Here’s how to update/create habtm (has_and_belongs_to_many) relationships with checkboxes.
app/model/user.rb:
<pre>
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
</pre>
app/controllers/users_controller.rb:
<pre>
class UsersController < ApplicationController
# There's nothing special about this update method from the
# standard scaffold generated update method. All the "magic"
# is in the automatically generated group_ids= method in the
# model called by update_attributes.
def update
@user = User.find(@params['id'])
user.update_attributes(params‘user’)
flash‘notice’ = “Update Successful”
else
render_action ‘edit’
end
end
end
app/views/users/form.html:
<pre>
<ul>
<% Group.find(:all).each do |g| %>
<li><input type="checkbox" name="user[groupids][]"
value=“<%= g.id %>”
<% if @user.groups.include?(g) > checked=“checked” < end > />
<= g.name %>
<% end %>
alternatively, the checkbox can be represented by a helper:
<pre>
<ul>
<% Group.find(:all).each do |g| %>
<li><%= check_box_tag("user[group_ids][]",
g.id,
@user.groups.include?(g)) %>
<%= g.name %>
<% end %>
What this view does is dynamically generate a checkbox for every group. The name of the input is significant obviously. The trailing “[]” on the name means the end result will be the list of checked ids. This list will be stored on the @params['user'] hash with the key ‘groups_ids’. When the controller calls @user.update_attributes(params‘user’)@, it tries to call @user.key= for each of the keys on @params['user']. What’s important to realize is that these keys don’t have to actually be attributes on the User model. All that’s important is that there’s a key= method on the model. Since Rails 0.13, the model automatically contains a “collection_ids=” method for habtm and has-many associations.
This method will load the objects identified by the ids and call the “group=(list)” method on the model with the freshly loaded list. This method in turn, will compare the list to the current list of groups and delete/add groups as necessary.
skanthak (2005-07-24): This has been updated for Rails 0.13. Is it working for people now?
This was originally posted in MultipleSelectOptionsHelper.
Then discussed on the rails listserv
And now compiled here.
I tried this to the letter but kept getting this error.
ActiveRecord::AssociationTypeMismatch in Users#update
Group expected, got Array
Instead i tried the method here and it works correctly. looks like the id in the array is not getting converted to the type of the array element, in this case Group.
http://www.lathi.net/twiki-bin/view/Main/HabtmCheckbox?skin=print
my system is:
rails 0.13.1
win32
There was a syntax error in has_group? ( missing end ).
fixed in case anyone was having a problem.
Works for me! Thanks -JamesM
I had to add this code above the update_attributes call in the controller’s update method for unchecking all groups to actually clear the group records:
<pre>
class UsersController < ApplicationController
def update
@user = User.find(@params['id'])
user.update_attributes(params‘user’)
flash‘notice’ = “Update Successful”
else
render_action ‘edit’
end
end
end
When update_attributes is called, it will only call the group_ids= function if params['user']['group_ids'] exists. Can someone confirm this before I merge this code into the controller’s update method above? – JesseNewland
Jesse is right. If all the checkboxes are unchecked (i.e. if you want to remove a user from all the groups) the group_ids= fuction is never called and thus the changes are not updated.
Javier Smaldone
So where is the group stuff? how would you store permissions for a group and a user using one table for storing the actual permissions and foreign keys?
Thanks for this tutorial, it was sooo easy. I’m truly amazed. :) – Ian Neubert
Here’s a little helper function….
def collection_check_boxes object, method, collection, value_method, text_method
collection.collect do |c|
check_box_tag("#{object}[#{method.singularize}_ids][]", c.send(value_method), eval("@#{object}").send(method).include?(c)) + " #{c.send(text_method)}"
end.join ' '
end
This doesn’t seem to work for inserting new records, only for updating existing records. When I create a new item and try setting the foo_ids where foo is a many-to-many, it doesn’t get saved to the database. When I update a record that already exists in the database, it works (the join records are created). To get it to work, I need to re-fetch the newly create row by its id, then call update_attributes on it.
def create
department = Department.new(params[:department]) # FIX! can't set many-to-many relationships on creation if @department.save Department.find(department.id).update_attributes(params[:department]) # hack to get around many-to-many not saving until object has an id
flash[:notice] = ‘Department was successfully created.’
redirect_to :action => ‘list’
else
render :action => ‘new’
end
end
Submitted this as a bug: http://dev.rubyonrails.org/ticket/3692
Sam Barnum?
I noticed the same thing after upgrading to Rails 1.0. I was on 0.13.1 and the habtm checkboxes worked flawlessly using the techniques described above. Now I have had to add the hack that Sam describes in order to get the join ids to save on ‘create’. Anyone else having this problem?
I had the same problem and the hack fixed it.
This is probably a new bug, this broke with 1.0.
def groups_ids=(list)
groups.clear
groups << Group.find(list)
end
Here is my workaround to Sam’s problem. It seems that this is not a problem with IDs at all. The way I worked around it would indicate that it is a bug in the validation code.
#workaround for bug #3692
@@user_ids = nil
def before_validation_on_create
unless self.users.empty?
@@user_ids = self.users.collect { |user| user.id }
self.users.clear
end
end
def after_validation_on_create
unless @@user_ids == nil
self.user_ids = @@user_ids
@@user_ids = nil
end
end
You may want to consider using update_attribute rather then update_attributes so that you avoid validating the original object when saving since it’s not being changed. In my case I’m using a seperate form to assign the related object to the user so the only information in the params is an array of the :group_ids rather than a complete user object + group_ids. In this situation you can fail some types of validation resulting it update_attributes returning false.
To avoid this the update function should look like:
def update_groups
@user = User.find(@params['id'])
if !params[:user]
@user.groups.clear
else
if @user.update_attribute(:group_ids,@params['user']['group_ids'])
flash['notice'] = "Update Successful"
else
render_action 'edit'
end
end
end
I had to do this in my app since update_attributes was always returning false because my user model has a validates_confirmation_of :password validation in it. – Kevin Kolk
Note that if you use a table named ‘attribute’ you will be forced to deal with a name space issue -PeterLD
(Rolled back to unspammed Version — Phillip)x2
(And again – Láďa)