Ruby on Rails - Save form data of one model in rel

2019-09-17 11:16发布

I have revised the question yet again to include the controller files.

In our app we have three models. A User model, Scoreboard model, Team model. A User has_many scoreboards and a scoreboard belongs_to a User. The code in the scoreboards controller for the create action that associated the two is "@scoreboard = current_user.scoreboards.build". This code works perfectly fine.

Now, the problem arises with the third model. The scoreboard model has_many teams and each team belongs_to a scoreboard. It is a has_many, belongs_to relationship. Therefore, the foreign key is on the teams table. The scoreboard and team migration and model files are given below respectively.

Scoreboard Model

class Scoreboard < ActiveRecord::Base
  belongs_to :user
  has_many :teams, dependent: :destroy
  default_scope -> { order(created_at: :desc) }
end

Scoreboard Migration

class CreateScoreboards < ActiveRecord::Migration
  def change
    create_table :scoreboards do |t|
      t.string :name_of_scoreboard
      t.string :name_of_organization
      t.string :name_of_activity
      t.references :user, index: true

      t.timestamps null: false
    end
    add_foreign_key :scoreboards, :users
    add_index :scoreboards, [:user_id, :created_at]
  end
end

Team Model

class Team < ActiveRecord::Base
  belongs_to :scoreboard
end

Team Migration

class CreateTeams < ActiveRecord::Migration
  def change
    create_table :teams do |t|
      t.string :name
      t.integer :win
      t.integer :loss
      t.integer :tie
      t.references :scoreboard, index:true

      t.timestamps null: false
    end
    add_foreign_key :teams, :scoreboards
  end
end

I think I have associated the models correctly. Therefore, the code in my Teams controller for the create action should create the associations correctly. The controllers are as follows:

Scoreboard controller:

class ScoreboardsController < ApplicationController

 before_action :logged_in_user, only: [:new, :create, :show, :index]
 before_action :correct_user, only: [:destroy, :edit, :update]

 def new
   @scoreboard = Scoreboard.new
 end

 def create
  @scoreboard = current_user.scoreboards.build(scoreboard_params)
  if @scoreboard.save
   flash[:scoreboard] = "Scoreboard created successfully"
   redirect_to scoreboard_path(@scoreboard)
  else
   render 'new'
  end
 end

 def show
  @scoreboard = Scoreboard.find_by_id(params[:id])
 end

  def index
    if params[:search]
      @scoreboards = Scoreboard.all.search(params[:search])
    else
      @scoreboards = current_user.scoreboards
    end
  end

 def edit
  @scoreboard = Scoreboard.find_by_id(params[:id])
 end

 def update
  @scoreboard = Scoreboard.find_by_id(params[:id])
  if @scoreboard.update_attributes(scoreboard_params)
   flash[:success] = "Updated Successfully"
   redirect_to scoreboard_path(@scoreboard)
  else
   render 'edit'
  end
 end

 def destroy
  @scoreboard = Scoreboard.find_by_id(params[:id])
  @scoreboard.destroy
  flash[:success] = "Deleted Successfully."
  redirect_to scoreboards_path
 end

private

  def scoreboard_params
   params.require(:scoreboard).permit(:name_of_scoreboard, :name_of_organization, 
                  :name_of_activity, :starts_at, :ends_at, :cities, :states, :country, :picture ) 
  end

   def correct_user
     @user = Scoreboard.find(params[:id]).user
     redirect_to scoreboards_path unless current_user?(@user)
   end

end 

And this is the teams controller:

class TeamsController < ApplicationController

def new
    @team = Team.new
  end

  def create
    @scoreboard= current_user.scoreboards.build
    @team = @scoreboards.teams.build(team_params)
    if @team.save
      flash[:success] = "Saved Successfully"
      redirect_to scoreboard_path
    else
      render 'new'
    end
  end

  def index

  end

  def show

  end

  private

  def team_params
    params.require(:team).permit(:name, :win, :loss, :tie)
  end

end

However, I get an error "undefined method `teams' for nil:NilClass" when I submit my form which applies the create action. I am not sure why this is happening because I did the exact same thing with the Users and Scoreboard model.

2条回答
干净又极端
2楼-- · 2019-09-17 11:56

There are many ways in Rails to create associated records via user input. Let's assume in this case that you only want to create teams in the context of a scoreboard.

The first thing we want to do is nest the teams routes under a scoreboard:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  resources :scoreboards do
    resources :teams, shallow: true
  end
end

This will give us routes to create teams which are nested:

             Prefix Verb   URI Pattern                                     Controller#Action
   scoreboard_teams GET    /scoreboards/:scoreboard_id/teams(.:format)     teams#index
                    POST   /scoreboards/:scoreboard_id/teams(.:format)     teams#create
new_scoreboard_team GET    /scoreboards/:scoreboard_id/teams/new(.:format) teams#new
          edit_team GET    /teams/:id/edit(.:format)                       teams#edit
               team GET    /teams/:id(.:format)                            teams#show
                    PATCH  /teams/:id(.:format)                            teams#update
                    PUT    /teams/:id(.:format)                            teams#update
                    DELETE /teams/:id(.:format)                            teams#destroy

Why?, because this gives a RESTful design where it is very obvious that you are creating a nested resource. Nesting the index route is really optional.

If you want a non nested index which displays all the teams regardless of scoreboard you would define the routes as such:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  resources :teams, only: :index
  resources :scoreboards do
    resources :teams, shallow: true, except: :index
  end
end

As you alread have guessed we want to build a Team instance in Scoreboards#show

# GET /scoreboards/1
def show
  @team = @scoreboard.teams.build
end

We also need to add a team form to the scoreboard. Let's use a reusable partial for this:

# app/views/scoreboards/show.html.erb 
<%= render partial: 'teams/form' %>

# app/views/teams/_form.html.erb
<%= form_for(@team.new_record? ? [@scoreboard, @team] : @team ) do |f| %>
  <% if @team.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@team.errors.count, "error") %> prohibited this team from being saved:</h2>

      <ul>
      <% @team.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :wins %><br>
    <%= f.number_field :wins %>
  </div>
  <div class="field">
    <%= f.label :loss %><br>
    <%= f.number_field :loss %>
  </div>
  <div class="field">
    <%= f.label :tie %><br>
    <%= f.number_field :tie %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Notice @team.new_record? ? [@scoreboard, @team] : @team. We use the ternary operator (a compact if statement) so that the form routes correctly both for a new record and when editing a record.

So in our TeamsController we want to setup a callback to load the Scoreboard from params[:schoolboard_id] so that we can use it our new and create action.

class TeamsController < ApplicationController

  before_action :set_scoreboard, only: [:new, :create]
  before_action :set_team, only: [:show, :edit, :update, :destroy]

  # GET /scoreboard/:scoreboard_id/teams
  def index
    @scoreboard = Scoreboard.eager_load(:teams)
                            .find(params[:scoreboard_id])
    @teams = @scoreboard.teams
  end

  # GET /teams/1
  def show
  end

  # GET /scoreboard/:scoreboard_id/teams/new
  def new
    @team = @scoreboard.teams.new
  end

  # POST /scoreboard/:scoreboard_id/teams
  def create
    @team = @scoreboard.teams.new(team_params)

    if @team.save
      redirect_to @team, notice: 'Team was successfully created.'
    else
      render :new
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_team
      @team = Team.find(params[:id])
    end

    def set_scoreboard
      @scoreboard = Scoreboard.find(params[:scoreboard_id])
    end

    # Only allow a trusted parameter "white list" through.
    def team_params
      params.require(:team).permit(:name, :wins, :loss, :tie, :scoreboard_id)
    end
end
查看更多
贼婆χ
3楼-- · 2019-09-17 12:20

I fixed up my answer in response to your question:

I think that your associations are correct and maybe try using @scoreboard instead of scoreboard. So something like this maybe?

def blah
    .... some code ....
    @scoreboard = current_user.scoreboards.build
    @team = @scoreboard.teams.build
    .... more code ....
end
查看更多
登录 后发表回答