pshear0
pshear0

Reputation: 213

Rails 3.2.2 Simple Form Nested Model (has_many through) works but want to improve

Using the simple_forms gem in Rails 3.2.2, I have built a form to create objects in a nested model (kudos again to Ryan Bates over at railscasts.com) but I am struggling to master the complexity of a has_many through relationship without adding extra code to the create method of my Games controller.

I am using accepts_nested_attributes_for :participants in my Game model with the hope of being able to mass assign the 2 required participant objects.

I have three classes: a Game, a Team, and a through table Participant

Participant exists so a team can be noted as the home or away team in a game.

Currently, I am using the select_tag in the form to create a couple of psuedo params that I then use to create the participants in the create method in the controller after I have saved the new @game object I am creating a @game.participant for the home team and the away team. This all works but is a bit too heavy lifting and can't be extended if I wanted, for example, to create a tournament of games without writing more controller code.

So now I build 2.times participants in the new method and am trying to get create and update to work without adding extra code and removing the creating @games.participants in the create method.

Ok ... the problems...

  1. builder.input :team_id, :collection => Team.all

    This puts the object id in the selector, I want the Team.full_name as is produced in the current select_tag "home", options_from_collection_for_select(Team.all, :id, :full_name) and the id to be pushed into the team_id param. I am struggling to find decent simple_forms documentation and looking through the gem source has given me a headache (I'm not good enough to decode it)

  2. I need to differentiate between the two participants created in the new Game object. The simple_form f.simple_fields_for :participants do |builder| won't allow me to do :participants.first or :participants.last. I want to create a hidden field setting the :home_team to true for the first participant.

If I can get these two pieces working then I think my solution would work. Any help or clues are, as always, hugely appreciated.

Game Model:

class Game < ActiveRecord::Base
  belongs_to :venue
  belongs_to :game_type
  has_and_belongs_to_many :tournaments
  has_many :participants, :dependent => :destroy
  has_many :teams, :through => :participants

  validates :kickoff, :venue_id, :presence => true
  accepts_nested_attributes_for :participants,
    :reject_if => lambda {|a| a[:game_id].blank? },
       :allow_destroy => :true
  def home_team
    self.teams.joins(:participants).where("participants.home_team = ?", true).first
  end
  def away_team
    self.teams.joins(:participants).where("participants.home_team = ?", false).first
  end
end

Participant Model:

class Participant < ActiveRecord::Base
  belongs_to :game
  belongs_to :team
end

Team Model:

class Team < ActiveRecord::Base
  has_many :participants, :dependent => :destroy
  has_many :games, :through => :participants
  validates :full_name, :presence => true
  validates :short_name, :presence => true, :length => { :within => 1..5 }
end

Games Controller (New and Create Methods)

class GamesController < ApplicationController
  def new
    @game = Game.new
    ### This section is not currently implemented
    # 2.times { @game.participants.build }
    ### End of unimplemented section
    respond_to do |format|
      format.html # new.html.erb
    end
  end

  def create
    @game = Game.new(params[:game])
    respond_to do |format|
    if @game.save
      @game.participants.create(:team_id => params[:home], :home_team => true)
      @game.participants.create(:team_id => params[:away], :home_team => false)
      format.html { redirect_to games_path, notice: 'Game was successfully created.' }
    else
      format.html { render action: "new" }
    end
  end
end

Games View Form Partial (actually has another partial but included for ease of reading)

<%= simple_form_for(@game) do |f| %>
  <%= f.error_notification %>   
  <div class="form-inputs">
    <%= label_tag "home", "Home Team"%>
    <%= select_tag "home", options_from_collection_for_select(Team.all, :id, :full_name)%>
    <!-- This section is not working properly -->
    <!-- <%=# f.simple_fields_for :participants do |builder| %> -->
      <!-- <%=# builder.input :team_id, :collection => Team.all, :selected => :full_name %> -->
    <!-- <%# end %> -->
    <!-- End of not working properly section -->
    <%= f.input :home_goals, :input_html => {:maxlength => 5, :size => 5 }%>
    <%= f.input :away_goals, :input_html => {:maxlength => 5, :size => 5 }%>
    <%= select_tag "away", options_from_collection_for_select(Team.all, :id, :full_name)%>
    <%= label_tag "away", "Away Team"%>
    <%= f.input :kickoff %>
    <%= f.input :completed %>
    <%= f.input :game_type_id %>
    <%= f.input :venue_id ,:collection => Venue.all, prompt:  'Choose the venue'%>
  </div> 
  <div class="form-actions">
    <%= f.button :submit %>
  </div>
<% end %>

Upvotes: 2

Views: 3846

Answers (1)

pshear0
pshear0

Reputation: 213

Okay ... in case anyone was interested I solved the two issues with the following:

  1. Used the collect capabilities on the class rather than any simple_form helpers, so the Games View Form Partial had the following changes to the collection line:

    <%= builder.input :team_id, :collection => Team.all.collect{ |t| [t.full_name, t.id ]}%>
    
  2. I used a count comparison to set the veriable noting the value of the hidden field. This is not very object code but means that the code works using simple_form helpers and the controller just uses the .save (in create) and update_attributes (in update) without any further code required. Very nice. Also slightly amended the accepts_nested_attributes_for to look for blank team_id rather than game_id (which would never be blank!). So the code now looks as follows...

Game Model:

    class Game < ActiveRecord::Base
      belongs_to :venue
      belongs_to :game_type
      has_and_belongs_to_many :tournaments
      has_many :participants, :dependent => :destroy
      has_many :teams, :through => :participants

      validates :kickoff, :venue_id, :presence => true

      accepts_nested_attributes_for :participants,
        :reject_if => lambda {|a| a[:team_id].blank? },
          :allow_destroy => :true

      def home_team
        self.teams.joins(:participants).where("participants.home_team = ?", true).first
      end

     def away_team
       self.teams.joins(:participants).where("participants.home_team = ?", false).first
     end
   end

Games Controller (New and Create Methods)

    class GamesController < ApplicationController
      def new
        @game = Game.new
        2.times { @game.participants.build }
        respond_to do |format|
          format.html # new.html.erb
        end
      end
      def create
        @game = Game.new(params[:game])
        respond_to do |format|
          if @game.save
            format.html { redirect_to games_path, notice: 'Game was successfully created.' }
          else
            format.html { render action: "new" }
          end
        end
      end
    end

Games View Form Partial (actually has another partial but included for ease of reading)

    <%= simple_form_for(@game) do |f| %>
      <%= f.error_notification %>
    <div class="form-inputs">
      <% count = 0 %>
      <%= f.simple_fields_for :participants do |builder| %>
        <% if count == 0 %>
          <% value = true %>
          <% count += 1 %>
        <% else %>
          <% value = false%>
        <% end %>
        <%= builder.input :home_team, :as => :hidden, :input_html => {:value => value} %>
        <%= builder.input :team_id, :collection => Team.all.collect{ |t| [t.full_name, t.id ]}%>
      <% end %>
      <%= f.input :home_goals, :input_html => {:maxlength => 5, :size => 5 }%>
      <%= f.input :away_goals, :input_html => {:maxlength => 5, :size => 5 }%>
      <%= f.input :kickoff %>
      <%= f.input :completed %>
      <%= f.input :game_type_id %>
      <%= f.input :venue_id ,:collection => Venue.all, prompt:  'Choose the venue'%>
    </div>
    <div class="form-actions">
       <%= f.button :submit %>
    </div>
  <% end %>

Hope this is of use to someone. I think it's a good solution but would be interested if anyone has suggestions on improving the logic in the View.

Regards

Peter

Upvotes: 3

Related Questions