Gold Masta
Gold Masta

Reputation: 725

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.

Upvotes: 0

Views: 829

Answers (2)

Gold Masta
Gold Masta

Reputation: 725

Thanks Pablo, I finally got it working. Your code didn't quite work but it put me on the right path. I changed the model associations around as you suggested. That does make more sense.

Here are my models:

class Answer < ApplicationRecord
    belongs_to :question
    has_many :responses, dependent: :destroy
end

class Examination < ApplicationRecord
  belongs_to :user
  has_many :questions, dependent: :destroy
  has_many :answers, :through => :questions
  has_many :scores
  has_secure_password
end

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

    accepts_nested_attributes_for :answers, allow_destroy: true, :reject_if => :all_blank

    validates_presence_of :body
    validates_presence_of :question_type
end

class Response < ApplicationRecord
  belongs_to :score
  belongs_to :answer
  belongs_to :question
end

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

  accepts_nested_attributes_for :responses, allow_destroy: true, reject_if: :no_answer_id? 

  private

 def no_answer_id?(att)
  att['answer_id'].blank?
 end
end

I had to add that special method to the Score model to account for unchecked responses. Otherwise, it would throw an error.

I moved the "take a test" logic and view to the Score controller. With your code, I was getting a double loop (questions listed multiple times). I learned that you can actually access the responses through the "form_for" form loop using "res_f.object". That's pretty cool.

I also had to add a hidden field on the radio button collection form to get the question id.

Here it is:

<%= link_to "Back to all exams", examinations_path %><br/>

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

<%= form_for [@exam, @score] do |f| %>
  <%= f.hidden_field :user_id, value: current_user.id  %>

    <%= f.fields_for :responses do |res_f| %>

      <h2>Question # <%= res_f.object.question.position %></h2>

      <span style="font-size: 24px; font-weight: normal">
        (<%= res_f.object.question.points %> Points)
      </span>
      <hr> 
      <h3><%= res_f.object.question.body %></h3>
      <p><%= res_f.collection_radio_buttons :answer_id, res_f.object.question.answers, :id, :body do |b| %></p>
        <div>
          <%= b.radio_button %>
          <%= b.label %>
          <%= res_f.hidden_field :question_id, value: res_f.object.question.id %>

        </div>
      <% end %>
    <% end %>


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

And the Scores controller:

class ScoresController < ApplicationController
    def new
        @exam = Examination.find(params[:examination_id])
        @score = @exam.scores.build(user_id: current_user.id)
        @exam.questions.each do |question|
            res = @score.responses.build(question_id: question.id)
            logger.info question.id
            logger.info res
        end
    end

  def create
    @exam = Examination.find(params[:examination_id])

    @score = @exam.scores.build(score_params)
    if @score.save
        redirect_to examination_path(@exam)
    else
      logger.info @score.errors.full_messages
      redirect_to root_path
    end
  end

   protected

      def score_params
        params.require(:score).permit(:examination_id, :user_id,
          responses_attributes: [:id, :answer_id, :question_id, :selected])
      end
end

This all works fine if there is only one correct answer. I'll have to modify it later to account for multiple answers. At least it works! I'll give you the credit Pablo.

Cheers

Upvotes: 1

Pablo
Pablo

Reputation: 3005

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)

Upvotes: 1

Related Questions