Rails .build 没有构建 has_many :options

Rails .build is not building has_many :options

我有一个包含 3 个模型的投票应用程序。

Poll.rb

class poll < ApplicationRecord
  validates_presence_of :user, :title
  belongs_to :user
  has_many :questions, dependent: :destroy
  has_many :options, through: :questions

  accepts_nested_attributes_for :questions
end

Question.rb

class Question < ApplicationRecord
  validates_presence_of :poll_id, :question_id, :title
  belongs_to :poll
  has_many :options
  accepts_nested_attributes_for :options, reject_if: proc { |attributes| attributes['title'].blank? }
end

Option.rb

class Option < ApplicationRecord
  validates_presence_of :question_id, :title
  belongs_to :question
  belongs_to :poll
end

我希望问题表单有一个用于添加选项的字段,所以我已将其添加到问题 _form 中。

  <%= form.fields_for :option do |o| %>
    <div>
      <%= o.label "Option", style: "display: block" %>
      <%= o.text_field :title, placeholder: "Enter Option here" %>
    </div>
  <% end %>

我现在可以看到一个很好的选项块。虽然我希望有 3 个可能的选项,所以在 questions_controller.rb 中我添加了以下内容:

 def new
    @question = @poll.questions.build
    3.times { @question.options.build } # 3 different options 
  end

尽管如此,我只看到一个选项块而不是 3 个。为什么会这样,我该如何解决? 此外,我没有看到新条目选项 postgresql table.


完整 questions_controller.rb

class QuestionsController < ApplicationController
  before_action :set_question, only: %i[ show edit update destroy ]
  before_action :set_poll

  # GET /questions or /questions.json
  def index
    @questions = Question.all
  end

  # GET /questions/1 or /questions/1.json
  def show
  end

  # GET /questions/new
  def new
    # @question = Question.new
    @question = @poll.questions.build
    3.times { @question.options.build } # 5 different options 
  end
  
  # GET /questions/1/edit
  def edit
  end

  # POST /questions or /questions.json
  def create
    @question = Question.new(question_params)
    

    respond_to do |format|
      if @question.save
        format.html { redirect_to polls_question_url(@question), notice: "Question was successfully created." }
        format.json { render :show, status: :created, location: @question }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /questions/1 or /questions/1.json
  def update
    respond_to do |format|
      if @question.update(question_params)
        format.html { redirect_to polls_question_url(@question), notice: "Question was successfully updated." }
        format.json { render :show, status: :ok, location: @question }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /questions/1 or /questions/1.json
  def destroy
    poll_id = Question.find_by(params[:poll_id])
    session[:return_to] ||= request.referer
    @question.destroy

    respond_to do |format|
      format.html { redirect_to session.delete(:return_to), notice: "Question was successfully destroyed." }
      format.json { head :no_content }
    end
  end

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

    # Only allow a list of trusted parameters through.
    def question_params
      params.require(:question).permit(:poll_id, :question_type, :title, :description, :randomize_selection, :voter_abstain, { option_attributes: [:question_id, :poll_id, :party_id, :title, :description] } )
    end

    def set_poll
      @poll = poll.find_by(params[:poll_id])
    end

end

routes.rb

  resources :users do
    resources :polls
  end
  resource :polls do
    resources :questions 
  end

  resource :questions do
    resources :options
  end

编辑:


这是我的部分问题。

_form.html.erb

<%= form_with(model: [@Poll, question] ) do |form| %>
  <% if question.errors.any? %>
    <div style="color: red">
      <h2><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>
      <ul>
        <% question.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div>
    <%= form.hidden_field :poll_id %>
  </div>

  <div>
    <%= form.label :question_type, style: "display: block" %>
    <%= form.text_field :question_type %>
  </div>

  <div>
    <%= form.label :title, style: "display: block" %>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :description, style: "display: block" %>
    <%= form.text_area :description %>
  </div>

  <div>
    <%= form.label :randomize_selection, style: "display: block" %>
    <%= form.check_box :randomize_selection %>
  </div>

  <div>
    <%= form.label :voter_abstain, style: "display: block" %>
    <%= form.check_box :voter_abstain %>
  </div>

  <div>
  <%= form.fields_for :options do |o| %>
    <div>
      <%= o.label "Option", style: "display: block" %>
      <%= o.text_field :title, placeholder: "Enter Option here" %>
    </div>
  <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

这是我渲染表格的民意调查显示。

show.html.erb

<p style="color: green"><%= notice %></p>

<p>
  <strong>Poll Title:</strong>
  <%= @poll.title %>
  <%= render @poll %>
</p>
<div>
  <%= link_to "Edit this poll", edit_user_poll_path(@poll) %> |
  <%= link_to "Back to polls", user_polls_path %> |
  <%= link_to "Destroy this poll", user_poll_path(@poll), method: :delete %>
</div>

<% if @poll.questions.any? %>
  <hr>
  <h2>Questions:</h2>
  <%= render @poll.questions %>
  
<% end %>
<hr>


<h2>Add a new Question:</h2>
<%= render "questions/form", question: @poll.questions.build %>

您传递给 fields_for 的参数必须与模型上的关联名称相匹配:

<%= form.fields_for :options do |o| %>
  <div>
    <%= o.label "Option", style: "display: block" %>
    <%= o.text_field :title, placeholder: "Enter Option here" %>
  </div>
<% end %>

请特别注意 Rails 中的复数化。这是让 Convention over Configuration 为您工作而不是对您不利的重要组成部分。

但是这段代码还有很多其他问题。

  • 常量应始终为 Ruby 中的 CamelCaseUPPERCASE - 您需要将 class poll 更改为 class Poll 并修复对 class。这不仅仅是样式问题,因为解释器会完全不同地处理以大写字母开头的标识符。
  • 你没有正确嵌套它。您有一个嵌套路由,但您仍然将其视为控制器和文档字符串中的 non-nested 资源。
  • 您正在传递参数白名单中的父 ID。 :poll_id:question_id 不应列入白名单。不要传递带有隐藏输入的父 ID。问题 ID 由 Rails 分配 - 你不应该相信用户会传递它。
  • 该选项不需要 poll_id。使用间接 has_one 关联向上树。这可能会导致问题及其选项属于不同民意调查的边缘情况。

首先让我们修复模型:

class Poll < ApplicationRecord
  # belongs_to assocations are required by default
  # adding validations will just cause duplicate error messages
  validates_presence_of :title
  belongs_to :user
  has_many :questions, dependent: :destroy
  has_many :options, through: :questions
  accepts_nested_attributes_for :questions
end

class Question < ApplicationRecord
  validates_presence_of :title
  belongs_to :poll
  has_many :options
  accepts_nested_attributes_for :options, reject_if: proc { |attributes| attributes['title'].blank? }
end

class Option < ApplicationRecord
  validates_presence_of :title
  belongs_to :question
  has_one :poll, through: :question
end

那我推荐你用shallow nesting

resource :polls do
  resources :questions, shallow: true
end

这会创建没有 /polls/:poll_id 前缀的问题 member routes(显示、编辑、删除),而 collection routes (index, create, new) 是嵌套的。

并且您将控制器设置为:

class QuestionsController < ApplicationController
  before_action :set_question, only: %i[ show edit update destroy ]
  before_action :set_poll, only: %i[ new create index ]

  # GET /polls/1/questions or /polls/1/questions.json
  def index
    @questions = @poll.questions.all
  end

  # GET /questions/1  or /polls/1/questions/1.json
  def show
  end

  # GET /polls/1/questions/new
  def new
    # build is just an alias of new for legacy compatibility with Rails 2...
    # its about time that we ditch it
    @question = @poll.questions.new 
    3.times { @question.options.new } # 5 different options 
  end
  
  # GET /questions/1/edit
  def edit
  end

  # POST /polls/1/questions or /polls/1/questions.json
  def create
    @question = @poll.questions.new(question_params)
    respond_to do |format|
      if @question.save
        format.html { redirect_to @question, notice: "Question was successfully created." }
        format.json { render :show, status: :created, location: @question }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /questions/1 or /questions/1.json
  def update
    respond_to do |format|
      if @question.update(question_params)
        format.html { redirect_to @question, notice: "Question was successfully updated." }
        format.json { render :show, status: :ok, location: @question }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /questions/1 or /questions/2.json
  def destroy
    session[:return_to] ||= request.referer 
    @question.destroy
    respond_to do |format|
      format.html { redirect_to session.delete(:return_to), notice: "Question was successfully destroyed." }
      format.json { head :no_content }
    end
  end

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

    # Only allow a list of trusted parameters through.
    def question_params
      # do not write this in a single unreadable line
      params.require(:question).permit(
        :question_type, 
        :title, 
        :description,
        :randomize_selection, 
        :voter_abstain, 
        # do not wrap hash arguments in brackets 
        # as it will break if/when the `permit` method is changed to use real keyword arguments 
        # for has_many assocations the key naming convention is also plural_attributes
        options_attributes: [
          :party_id, 
          :title, 
          :description
        ]
      )
    end

    def set_poll
      @poll = Poll.find_by(params[:poll_id])
    end
end

这里的主要区别在于,您应该通过 URL 中的参数查找嵌套路由的轮询,并根据轮询实例(设置 poll_id)创建问题。

已添加:

您实际上并没有使用您在控制器中初始化的模型。如果你想从一个完全不同的动作中呈现表单,你需要在那里初始化实例变量:

class PollsController < ApplicationController

  def show
    @question = @poll.questions.new
    3.times { @question.options.new } # 5 different options ???
  end
  # ...
end
<%= render "questions/form", question: @question %>

在你的部分,你有一个偷偷摸摸的小错误。 Ruby区分大小写,所以@poll@Poll实际上是不同的变量。

irb(main):049:0> @foo = "bar"                                                                                                                                                                                 => "bar"
irb(main):050:0> @Foo
=> nil          

因为实例变量是 auto-vivified 你只是得到一个意外的 nil 而不是错误。你真正想要的是:

<%= form_with(model: [@poll, question] ) do |form| %>