rails simple_form with has_many => through (hmt) relationship

has_many => through and forms.
Ruby: 1.87+ (should be ok on 1.9.2)
Rails 3.0.9 (tested). Should work from Rails 2.3.3 onwards at least.
Developed on: 7/24/2011
Works? Yes …

It took a while to get this right (i.e. simple with little code).
I have has_many =>throughs (the newer and easier form of has-and-many-to-many or HABTM) that use a link model/table called UserMemberships.
The items are users and parks.
The form for user requires all parks to be listed (checked when actually member), then UserMemberships (the join table) determines which options are selected on the form's select input element.
The full implementation included devise but I stripped that out here for simplicity of example, just leaving attr_accessible for park_ids in case needed.
Routes:

GorillaRails::Application.routes.draw do
  resources :accounts
  resources :parks
end

Models:

class Park < ActiveRecord::Base
  has_many :user_memberships
  has_many :users, :through=>:user_memberships
end
class UserMembership < ActiveRecord::Base
  belongs_to :user
  belongs_to :park
end
class User < ActiveRecord::Base
  has_many :user_memberships
  has_many :parks, :through=>:user_memberships
  attr_accessible :park_ids # Needed when using Devise (not shown) and other authentication methods that require attr_accessible.
  after_save :update_parks # This set the park-user membership!
  def update_parks #after_save callback to handle park_ids
    unless park_ids.nil?
      self.user_memberships.each do |mbr|
        mbr.destroy unless park_ids.include?(mbr.park_id)  # First delete all the records.
      end
      park_ids.each do |park|
        self.user_memberships.create(:park_id => park) unless park.blank? # Now insert records as selected by user
      end
      reload
    end
  end  
end
class Account < ActiveRecord::Base # This is User (was set this way due to devise!)
end  

Controller:
account_controller.rb (for user, due to Devise)

  def new
    @parks = Park.all
    @account = User.new (-here its user)
    @account.attributes = params[:user]
    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @account }
    end
  end
  def edit
    @account = User.find(params[:id])
    @parks = Park.all
  end
  def create
    @account = User.new(params[:user]) # Create the new user
    respond_to ...
  def update
    @account = User.find(params[:id])
    respond_to do |format|
      if @account.update_attributes(params[:user]) ... 

Views(user):
views/accounts/_newform.html.haml

= simple_form_for @account, :url => accounts_path do |f|
  -if @account.errors.any?
    #error_explanation
      %h2= "Errors prohibited this user from being saved:"
  %br
  = render :partial => 'inputs_parks_submit_and_back', :locals => {:f => f}

views/accounts/_updateform.html.haml

= simple_form_for @account, :url => account_path do |f|
  -if @account.errors.any?
    #error_explanation
      %h2= "#{pluralize(@account.errors.count, "error")} prohibited this user from being saved:"
      %ul
        - @account.errors.full_messages.each do |msg|
          %li= msg
  = render :partial => 'inputs_parks_submit_and_back', :locals => {:f => f}

views/accounts/_input_parks_submit_and_back.html.haml

.inputs
  = f.input :email, :label => "Email:"
  ...
  = f.association :parks, :required => true, :collection => @parks, :as => :check_boxes, :label_method => lambda{ |park| "#{park.name} [#{park.country.name}]"}, :label => "Park:"
.actions
  = f.submit 'Save'
%br
= link_to 'Back', accounts_path
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s