Search code examples
ruby-on-railsformsnested-attributeshas-many

Create and Edit and Update won't work for 3rd laryer objects from Nested form on 1st layer in Rails 4.2


I'm making nested form with Rails 4.2. And I'm having problems with Create and Edit and Update actions on 3rd layer object Answer. I've summarized information in below. If you know what I'm doing wrong or did not do, please kindly let me know.

Scope:

  • User can create Survey and Questions and Answers.
  • User can edit/update Survey and Questions and Answers.
  • User can delete Survey and Questions and Answers

Issues: I've set up nested has_many associations between Survey > Questions > Answers and setup 'accepts_nested_attributes_for' and added model's attributes in permit parameters. And made Survey form that has fields for all models. It works fine for creating Survey with Questions with one submit from one form, and these two model's attributes appear in Show action as well. But Answer won't saved, and answer from doesn't appear in Edit form.

Code:

Models

Survey.rb

class Survey < ActiveRecord::Base
    has_many :questions, dependent: :destroy
    accepts_nested_attributes_for :questions, allow_destroy: true
end

Question.rb

class Question < ActiveRecord::Base
    belongs_to :survey
    has_many :answers, dependent: :destroy
    accepts_nested_attributes_for :answers, allow_destroy: true
end

Answer.rb

class Answer < ActiveRecord::Base
  belongs_to :question
end

Controllers

Surveys_controller.rb

class SurveysController < ApplicationController
  before_action :set_survey, only: [:show, :edit, :update, :destroy]

  # GET /surveys
  # GET /surveys.json
  def index
    @surveys = Survey.all
  end

  # GET /surveys/1
  # GET /surveys/1.json
  def show
  end

  # GET /surveys/new
  def new
    @survey = Survey.new
    @questions = @survey.questions.build
    @answers = @questions.answers.build
    #@survey.questions.build
    #@questions.answers.build
 end

  # GET /surveys/1/edit
  def edit
  #  @questions = @survey.questions.update(survey_params) #This will get ActionController::ParameterMissing in SurveysController#edit 
  #  @answers = @questions.answers.update(survey_params) #This will get ActionController::ParameterMissing in SurveysController#edit 
  end

  # POST /surveys
  # POST /surveys.json
  def create
    @survey = Survey.new(survey_params)
    respond_to do |format|
      if @survey.save
        format.html { redirect_to @survey, notice: 'Survey was successfully created.' }
        format.json { render :show, status: :created, location: @survey }
      else
        format.html { render :new }
        format.json { render json: @survey.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /surveys/1
  # PATCH/PUT /surveys/1.json
  def update
    respond_to do |format|
      if @survey.update(survey_params)
        format.html { redirect_to @survey, notice: 'Survey was successfully updated.' }
        format.json { render :show, status: :ok, location: @survey }
      else
        format.html { render :edit }
        format.json { render json: @survey.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /surveys/1
  # DELETE /surveys/1.json
  def destroy
    @survey.destroy
    respond_to do |format|
      format.html { redirect_to surveys_url, notice: 'Survey was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

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

    # Never trust parameters from the scary internet, only allow the white list through.
    def survey_params
    #  params.require(:survey).permit(:name, :description)
      params.require(:survey).permit(:name, :description, questions_attributes: [:id, :name, :description, :_destroy])
    #  params.require(:survey).permit(:name, :description, questions_attributes: [:id, :name, :description, :_destroy, answers_attributes: [:id, :content, :_destroy]]) #I don't see any change from addin ansewrs attributes here
    end
end

Questions_controller.rb

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

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

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

  # GET /questions/new
  def new
    @question = Question.new
    @question.answers.build
  end

  # GET /questions/1/edit
  def edit
  end

  # POST /questions
  # POST /questions.json
  def create
    @question = Question.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 }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /questions/1
  # PATCH/PUT /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 }
        format.json { render json: @question.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /questions/1
  # DELETE /questions/1.json
  def destroy
    @question.destroy
    respond_to do |format|
      format.html { redirect_to questions_url, 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

    # Never trust parameters from the scary internet, only allow the white list through.
    def question_params
      params.require(:question).permit(:name, :description, :survey_id, :_destroy, answers_attributes: [:id, :content, :_destroy])
    end
end

Answers_controller.rb

class AnswersController < ApplicationController
  before_action :set_answer, only: [:show, :edit, :update, :destroy]

  # GET /answers
  # GET /answers.json
  def index
    @answers = Answer.all
  end

  # GET /answers/1
  # GET /answers/1.json
  def show
  end

  # GET /answers/new
  def new
    @answer = Answer.new
  end

  # GET /answers/1/edit
  def edit
  end

  # POST /answers
  # POST /answers.json
  def create
    @answer = Answer.new(answer_params)

    respond_to do |format|
      if @answer.save
        format.html { redirect_to @answer, notice: 'Answer was successfully created.' }
        format.json { render :show, status: :created, location: @answer }
      else
        format.html { render :new }
        format.json { render json: @answer.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /answers/1
  # PATCH/PUT /answers/1.json
  def update
    respond_to do |format|
      if @answer.update(answer_params)
        format.html { redirect_to @answer, notice: 'Answer was successfully updated.' }
        format.json { render :show, status: :ok, location: @answer }
      else
        format.html { render :edit }
        format.json { render json: @answer.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /answers/1
  # DELETE /answers/1.json
  def destroy
    @answer.destroy
    respond_to do |format|
      format.html { redirect_to answers_url, notice: 'Answer was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

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

    # Never trust parameters from the scary internet, only allow the white list through.
    def answer_params
      params.require(:answer).permit(:content, :question_id)
    end
end

DB

schema.rb

ActiveRecord::Schema.define(version: 20160602204457) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "answers", force: :cascade do |t|
    t.string   "content"
    t.integer  "question_id"
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: false
  end

  add_index "answers", ["question_id", "created_at"], name: "index_answers_on_question_id_and_created_at", using: :btree
  add_index "answers", ["question_id"], name: "index_answers_on_question_id", using: :btree

  create_table "questions", force: :cascade do |t|
    t.string   "name"
    t.string   "description"
    t.integer  "survey_id"
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: false
  end

  add_index "questions", ["survey_id", "created_at"], name: "index_questions_on_survey_id_and_created_at", using: :btree
  add_index "questions", ["survey_id"], name: "index_questions_on_survey_id", using: :btree

  create_table "surveys", force: :cascade do |t|
    t.string   "name"
    t.string   "description"
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: false
  end

  add_foreign_key "answers", "questions"
  add_foreign_key "questions", "surveys"
end

Views

app/views/surveys/new.html.erb

<h1>New Survey</h1>

<%= render 'form' %>

<%= link_to 'Back', surveys_path %>

app/views/surveys/new.html.erb

<h1>Editing Survey</h1>

<%= render 'form' %>

<%= link_to 'Show', @survey %> |
<%= link_to 'Back', surveys_path %>

app/views/surveys/_form.html.erb

<%= form_for(@survey) do |f| %>
<%#= form_for(setup_survey(@survey)) do |f| %>
  <% if @survey.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@survey.errors.count, "error") %> prohibited this survey from being saved:</h2>

      <ul>
      <% @survey.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 :description %><br>
    <%= f.text_field :description %>
  </div>
    <%= f.fields_for :questions do |ff| %>
    <%= render 'question_fields', ff: ff %>
    <% end %>

    <%= link_to_add_fields "Add Question", f, :questions %> 

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

app/views/surveys/_question_fields.html.erb

<fieldset>
  <div class="field">
    <%= ff.label :name, "Question Name" %><br>
    <%= ff.text_field :name %><br>
  </div>
  <div class="field">
    <%= ff.label :description, "Question Description" %><br>
    <%= ff.text_field :description %>
  </div>
  <div class="field">
    <%= ff.check_box :_destroy %>
    <%= ff.label :_destroy, "Remove Question"  %>
  </div>
    <%= ff.fields_for :answers do |fff| %>
    <%= render 'answer_fields', fff: fff %>
    <% end %>

</fieldset>

app/views/surveys/_answer_fields.html.erb

<fieldset>
  <div class="field">
    <%= fff.label :content, "Answer" %>
    <%= fff.text_field :content %>
  </div>
  <div class="field">
    <%= fff.check_box :_destroy %>
    <%= fff.label :_destroy, "Remove Answer" %>
  </div>
</fieldset>

app/views/surveys/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @survey.name %>
</p>
<p>
  <strong>Description:</strong>
  <%= @survey.description %>
</p>

<ul>
<% @survey.questions.each do |question| %>
<li>
<%= question.name %><br>
<%= question.description %>
<% question.answers.each do |answer| %>
<%= answer.content %>
<% end %>
</li>
<% end %>
</ul>
<%= link_to 'Edit', edit_survey_path(@survey) %> |
<%= link_to 'Back', surveys_path %>

Thanks!


Solution

  • You should whitelist the answers_attributes in survey_params. Changing it like below should get you going.

    def survey_params
      params.require(:survey).permit(:name, :description, questions_attributes: [:id, :name, :description, :_destroy, answers_attributes: [:id, :content, :_destroy]])
    end