Reputation: 725
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
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
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