Search code examples
ruby-on-railscheckboxnested-formsnested-attributes

Check box that creates nested objects in rails


I'm building an app that creates exams. For the part where a user selects the answers on the exam, I want to use a checkbox (or a radio button) to allow them to pick the answer.

I want all the user-selected answers to be a table in itself called "responses". I can't figure out how to use a radio button to create records.

All the response record needs to do is take the ID's of the Exam, User, and Score. Score is a table that tracks the user's scores and the number of correct answers. Here's my examination model (rails wouldn't let me use the word "exam"). I have it set for nested attributes.

class Examination < ApplicationRecord
  belongs_to :user
  has_many :questions, dependent: :destroy
  has_many :scores
  has_many :responses
  has_secure_password

  accepts_nested_attributes_for :responses, allow_destroy: true
end

The response model is pretty basic:

class Response < ApplicationRecord
  belongs_to :user
  belongs_to :score
  belongs_to :examination
end

Here's the "take an exam" page: <%= link_to "Back to all exams", examinations_path %>

<h2><%= @exam.name %></h2>
<h3><%= @exam.intro %></h3>

<%= form_for @exam  do |f| %>
  <%= f.hidden_field :name, value: @exam.name  %>
  <%= fields_for :responses do |res_f| %>
    <% @exam.questions.each_with_index do |question, i| %>
      <% index = i + 1 %>
      <h2>Question #<%=index%></h2><span style="font-size: 24px; font-weight: normal">(<%= question.points %> Points)</span>
      <hr>
      <h3><%= question.body %></h3>
      <% question.answers.each do |ans| %>
        <table>
          <tr>
            <td><%= res_f.check_box :answer_id , ans.id, :examination_id , @exam.id, :user_id  %></td>
            <td><%= ans.body %></td>
          </tr>
        </table>
      <% end %>
    <% end %>
  <% end %>

  <%= f.submit 'Submit' %>
<% end %>

This code doesn't run because Rails expects the responses records to exist in order to use the form. It throws this error:

undefined method `merge' for 484:Integer

If I tweak that checkbox code to this:

<%= res_f.check_box :answer_id  %>

The code will run and it will give me the following params on submit:

Started PATCH "/examinations/34" for 127.0.0.1 at 2018-02-24 16:22:41 -0800
Processing by ExaminationsController#update as HTML
      Parameters: {"utf8"=>"✓", "authenticity_token"=>"y4vcPByUKnDdM6NsWDhwxh8MxJLZU4TQo+/fUrmKYEfb3qLn5FVieJAYirNRaSl0w5hJax20w5Ycs/wz1bMEKw==", "examination"=>{"name"=>"Samuel Smith’s Oatmeal Stout"}, "responses"=>{"answer_id"=>"1"}, "commit"=>"Submit", "id"=>"34"}

I know it's not right but I was hoping it would create a record at least. All the checkbox has to do it create a response record. It should be able to grab the answer_id, exam_id and user_id. That's it.

Does anyone know how to do this?

Edit in response to Pablo 7: Here are the other models (they're pretty basic right now)

class Score < ApplicationRecord
  belongs_to :user
  belongs_to :examination
  has_many :responses, dependent: :destroy
end

class User < ApplicationRecord
  has_many :examinations, dependent: :destroy
  has_many :scores, dependent: :destroy
  has_many :responses, dependent: :destroy

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

class Question < ApplicationRecord
    belongs_to :examination
    has_many :answers, dependent: :destroy

    accepts_nested_attributes_for :answers, allow_destroy: true

    validates_presence_of :body
    validates_presence_of :question_type
end

@exam and Examination are the same. There is a "take" action in the Examination controller that allows a user to take an exam:

def take
    @exam = Examination.find(params[:id])
    @score = @exam.scores.build
    @score.user_id = current_user.id
    @score.save
end

So an exam belongs to the user that created it. The same user or a different one can take an exam using the take action. They would then have a score that belongs to them.


Solution

  • I think you must do some changes to your models:

    • A Response should belong to a Question (it's the question the user is responding).

    • A Response should belong to an Answer (it's the correct Answer for the question; the one that the user checks). If you want to allow multiple correct answers, this should be changed.

    • A Response should not belong to an Examination and should not belong to a User. In fact, a Response belongs to a Score and that's enough because the Score already belongs to an Examination and to a User.

    • An Examination should not have many responses. In fact, an Examination has many scores and scores have many responses. If you want, you can use has_many :responses, through: :scores

    • A User should not have many Responses. They have many Scores and Scores have many Responses. If you want, you can use has_many :responses, through: :scores

    When you create a new score (in take), you should create empty responses for each question in the examination:

    def take
      @exam = Examination.find(params[:id])
      @score = @exam.scores.build(user_id: current_user.id)
      @exam.questions.each { |question| @score.responses.build(question_id: question.id) }
    
      #I don't think you should save here. 
      #This method is like the new method
      #You should save when the score is submitted
      #@score.save 
    end
    

    In your form: I would change the form to the score model (not examination). If you are using nested routes it could be [@exam, @score]

    This may have many errors, as I cannot test it right now. I hope the idea is clear:

    <%= form_for @score do |f| %>
      <%= f.hidden_field :name, value: @score.examination.name  %>
      <% @score.responses.each_with_index do |response, i| %>
        <%= f.fields_for response do |res_f| %>
          <% index = i + 1 %>
          <h2>Question #<%= index %></h2>
          <span style="font-size: 24px; font-weight: normal">
            (<%= response.question.points %> Points)
          </span>
          <hr>
          <h3><%= response.question.body %></h3>
          <%= res_f.collection_radio_buttons :answer_id, response.question.answers, :id, :body %>
        <% end %>
      <% end %>
    
      <%= f.submit 'Submit' %>
    <% end %>
    

    The submit should call a method in Score model to create a Score (ScoresController.create)