Building A Quiz with Ruby on Rails Using Multiple Instances of A Model

Tressa Sanders
6 min readApr 3, 2018

--

NEW ARTICLE ON THIS TOPIC HERE (7/27/2021): Building a Form with Ruby On Rails Using Multiple Instances of A Model (Reloaded)

When I built Stack English, I knew English proficiency assessments were going to be an essential part of the client relationship building process. We (the English trainers) would need to speak with our clients before booking the first lesson to find out their English proficiency level as well as what they would like to accomplish with our English lessons.

To determine their English proficiency level we would need to administer both a written and oral test. That meant I was going to have to create a quiz in Rails for the first time.

I’ll be honest, I was not looking forward to creating a quiz. I looked to see if someone had already invented the wheel and created a survey or quiz gem. Indeed they had but it was not going to do what I needed it to do. So I had no choice but to create the test from scratch.

So what did I need this quiz to do exactly?

The written quiz is 120 multiple choice questions. The English trainer will ask them one at a time until the client is no longer able to answer them. So I needed it to submit all of the answers at once even if all of the questions weren't answered and I needed it to only accept one answer per question.

So let’s get started with our database schema.

create_table “assessment_questions”, force: :cascade, options: “ENGINE=InnoDB DEFAULT CHARSET=utf8” do |t|
t.boolean “active”
t.integer “order”
t.string “level”
t.string “question”
t.string “letter”
t.string “answer”
t.datetime “created_at”, null: false
t.datetime “updated_at”, null: false
end

The “active” field allows me to ghost questions I don’t want to show in the survey without deleting them. The “order” field allows me to sort the questions. For my survey, each assessment question is specific to an English proficiency level so there is a “level” field. The “question” field is self explanatory. The “letter” and “answer” fields hold the values for the correct answer and the letter associated with it. I use these fields in the PDF assessment report and the index view. It’s up to you if you would need them or not.

create_table “assessment_options”, force: :cascade, options: “ENGINE=InnoDB DEFAULT CHARSET=utf8” do |t|
t.string “letter”
t.string “answer”
t.integer “assessment_question_id”
t.datetime “created_at”, null: false
t.datetime “updated_at”, null: false
t.index [“assessment_question_id”], name: “index_assessment_options_on_assessment_question_id”
end

Then I needed to create an entry for each option for each question. So the assessment options table contains the letter and text answer for the option as well as the id of the question it’s associated with.

create_table “assessment_answers”, force: :cascade, options: “ENGINE=InnoDB DEFAULT CHARSET=utf8” do |t|
t.string “letter”
t.string “answer”
t.boolean “correct”
t.integer “assessment_question_id”
t.integer “assessment_id”
t.datetime “created_at”, null: false
t.datetime “updated_at”, null: false
t.index [“assessment_id”], name: “index_assessment_answers_on_assessment_id”
t.index [“assessment_question_id”], name: “index_assessment_answers_on_assessment_question_id”
end

This table holds the client’s answer to each question. It includes the letter the text answer the client chose and the letter for that answer. There is also a field to indicate if the answer is correct or not and fields for the question and assessment ids.

Now we’ll need the following controllers: assessments, assessment_questions, assessment_options, assessment_answers. I’m only going to include the code that is relevant to the quiz. If you are creating your own quiz, what you name it and use it for will be different of course.

In the assessments_controller.rb:

def written
prepare_meta_tags title: “Written English Assessment Test”
set_meta_tags noindex: true
@assessment_questions = AssessmentQuestion.all
@assessment_options = AssessmentOption.all
@empty_answers = []

AssessmentQuestion.all.each do
@empty_answers << AssessmentAnswer.new
end
render :layout => ‘english’
end

This basically allows you to create 1 new empty answer for each question and store all of them in an array with the variable @empty_answers.

There is nothing special you need to do for assessment_questions.rb or assessment_options.rb. These controllers are only used for adding, updating and deleting assessment questions and their associated options. You could technically remove them and just use your database seed file to populate the questions and answers which is what I did but I still wanted to be able to manage it all in the admin panel after the fact.

In the assessment_answers.rb controller:

def create
params[“answers”].each do |key, value|
@assessment_answer = AssessmentAnswer.create(assessment_answer_params(value))
end

respond_to do |format|
if @assessment_answer.save
delete_written
format.html { redirect_to assessment_url(@assessment), notice: ‘Assessment answers were successfully created.’ }
format.json { render :show, status: :created, location: @assessment_answer }
else
format.html { render :new }
format.json { render json: @assessment_answer.errors, status: :unprocessable_entity }
end
end
end
def assessment_answer_params(answers)
answers.permit(:letter, :answer, :correct, :assessment_question_id, :assessment_id)
end

This code creates a new instance for each answer with the corresponding data submitted by the user.

Make a note of the slightly different code format for submitting parameters at the bottom of the controller.

You’ll notice there is a action called “delete_written”. Ideally it would be great if unanswered questions weren't saved at all but that’s not the case here. All 120 questions are saved even if some have empty answers. So the action “delete_written” removes the answers that have an empty “letter” field.

def delete_written
AssessmentAnswer.where(letter: nil).delete_all
end

I needed to add associations in the assessment, assessment_questions, assessment_options and assessment_answers models.

Assessment.rb

has_many :assessment_answers, dependent: :destroy, :inverse_of => :assessment
accepts_nested_attributes_for :assessment_answers

Assessment_questions.rb

has_many :assessment_options, dependent: :destroy, :inverse_of => :assessment_question
accepts_nested_attributes_for :assessment_options

has_many :assessment_answers, dependent: :destroy, :inverse_of => :assessment_question
accepts_nested_attributes_for :assessment_answers

Assessment_options.rb

belongs_to :assessment_question, :inverse_of => :assessment_options
accepts_nested_attributes_for :assessment_question

Assessment_answers.rb

belongs_to :assessment, :inverse_of => :assessment_answers
accepts_nested_attributes_for :assessment

belongs_to :assessment_question, :inverse_of => :assessment_answers
accepts_nested_attributes_for :assessment_question

I bet you want to know about routes now huh?

BAM!

resources :assessments
resources :assessment_questions do
resources :assessment_options
end
resources :assessment_answers
get ‘written’ => ‘assessments#written’, :as => ‘assessment/written_test’

Now we need a view that will allow us to create inputs for each of the empty answers in the array. For each empty answer we are also displaying all of the available options for each question as a radio button.

 <%= form_tag assessment_answers_path(method: :post), :name => ‘example-1’, :id => ‘wrapped’ do %>

<% @empty_answers.each do |answer| %>
<%= fields_for ‘answers[]’, answer, include_id: false do |r| %>

<% q = AssessmentQuestion.where(order: @empty_answers.index(answer) + 1).first %>

<%= r.collection_radio_buttons(:letter, q.assessment_options.order(“letter”), :letter, :answer, include_hidden: false) do |r| %>

<div class=”form-group radio_input”>
<%= r.radio_button(class: ‘icheck’, name: ‘answers[‘ + (q.order — 1).to_s + ‘][letter]’) + r.label(:class => ‘’) %>
</div>

<% end %>


<%= r.hidden_field :assessment_question_id, value: q.id, name: ‘answers[‘ + @empty_answers.index(answer).to_s + ‘][assessment_question_id]’ %>

<%= r.hidden_field :assessment_id, value: params[:assessment_id], name: ‘answers[‘ + @empty_answers.index(answer).to_s + ‘][assessment_id]’ %>

<% end %>

<% end %>

<div id=”bottom-wizard”>
<button type=”button” name=”backward” class=”backward”>Backward </button>
<button type=”button” name=”forward” class=”forward”>Forward</button>
<button type=”submit” name=”process” class=”submit”>Submit</button>
</div><! — /bottom-wizard →

<% end %>

That’s it. I know you might be all salty that I didn't include all the views. But that’s because what you do with your quiz views really depends on what you are using your quiz, test or survey for.

For example, my index view shows a list of only the questions that were answered and whether or not they are correct. It also shows the English proficiency level that is calculated after all of the answers have been submitted.

But your quiz, test or survey will be different so you have to figure out your views on your own.

Rails version: 5.1.3

NEW ARTICLE ON THIS TOPIC HERE (7/27/2021): Building a Form with Ruby On Rails Using Multiple Instances of A Model (Reloaded)

Have you built a quiz, test or survey with Rails? How? Let me know in the comments, chat with me on Twitter, LinkedIn or if you are looking to improve your Technical or Business English, subscribe to my newsletter for video mini-courses!

--

--

Tressa Sanders

Technical Writer, Senior English Trainer and Ruby on Rails Developer with over 20 years working in the Information Technology industry.