Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to associate a new model with existing models using has_and_belongs_to_many

I have two models with a many to many relationship using has_and_belongs_to_many. Like so:

class Competition < ActiveRecord::Base
  has_and_belongs_to_many :teams
  accepts_nested_attributes_for :teams
end

class Team < ActiveRecord::Base
  has_and_belongs_to_many :competitions
  accepts_nested_attributes_for :competitions
end

If we assume that I have already created several Competitions in the database, when I create a new Team, I would like to use a nested form to associate the new Team with any relevant Competitions.

It's at this point onwards that I really do need help (have been stuck on this for hours!) and I think my existing code has already gone about this the wrong way, but I'll show it just in case:

class TeamsController < ApplicationController
  def new
    @team = Team.new
    @competitions.all
    @competitions.size.times {@team.competitions.build}
  end
  def create
    @team = Team.new params[:team]
    if @team.save
      # .. usual if logic on save
    end
  end
end

And the view... this is where I'm really stuck so I won't both posting my efforts so far. What I'd like it a list of checkboxes for each competition so that the user can just select which Competitions are appropriate, and leave unchecked those that aren't.

I'm really stuck with this one so appreciate any pointing in the right direction you can provide :)

like image 948
aaronrussell Avatar asked Jan 21 '10 11:01

aaronrussell


1 Answers

The has_and_belongs_to_many method of joining models together is deprecated in favor of the new has_many ... :through approach. It is very difficult to manage the data stored in a has_and_belongs_to_many relationship, as there are no default methods provided by Rails, but the :through method is a first-class model and can be manipulated as such.

As it relates to your problem, you may want to solve it like this:

class Competition < ActiveRecord::Base
  has_many :participating_teams
  has_many :teams,
    :through => :participating_teams,
    :source => :team
end

class Team < ActiveRecord::Base
  has_many :participating_teams
  has_many :competitions,
    :through => :participating_teams,
    :source => :competition
end

class ParticipatingTeam < ActiveRecord::Base
  belongs_to :competition
  belongs_to :team
end

When it comes to creating the teams themselves, you should structure your form so that one of the parameters you receive is sent as an array. Typically this is done by specifying all the check-box fields to be the same name, such as 'competitions[]' and then set the value for each check-box to be the ID of the competition. Then the controller would look something like this:

class TeamsController < ApplicationController
  before_filter :build_team, :only => [ :new, :create ]

  def new
    @competitions = Competitions.all
  end

  def create
    @team.save!

    # .. usual if logic on save
  rescue ActiveRecord::RecordInvalid
    new
    render(:action => 'new')
  end

protected
  def build_team
    # Set default empty hash if this is a new call, or a create call
    # with missing params.
    params[:team] ||= { }

    # NOTE: HashWithIndifferentAccess requires keys to be deleted by String
    # name not Symbol.
    competition_ids = params[:team].delete('competitions')

    @team = Team.new(params[:team])

    @team.competitions = Competition.find_all_by_id(competition_ids)
  end
end

Setting the status of checked or unchecked for each element in your check-box listing is done by something like:

checked = @team.competitions.include?(competition)

Where 'competition' is the one being iterated over.

You can easily add and remove items from your competitions listing, or simply re-assign the whole list and Rails will figure out the new relationships based on it. Your update method would not look that different from the new method, except that you'd be using update_attributes instead of new.

like image 158
tadman Avatar answered Oct 31 '22 01:10

tadman