Demo 7: Working with Simple Models
In this demonstration, I will show how to create a model class and some sample objects, persist those objects in the database, and then retrieve and view them on a web page. I will continue to work on the QuizMe project from the previous demos.
1. Adding a New Model Class and Sample Data
Since we are building a quizzing application, we will need to store questions in the database. At first, the app will only allow multiple choice questions but in a later demo we will see that it’s pretty easy to add other kinds of questions too (e.g., fill in the blank). For multiple choice questions we need to store the question, answer, and a couple of incorrect options (distractors). Figure 1 shows the corresponding class diagram.
-
Run the following
rails generate model ...command to create the model class shown in the above class diagram:rails g model McQuestion question:string answer:string distractor_1:string distractor_2:stringNote the files that are generated, including
app/models/mc_question.rb(the model class),db/migrate/20190926192541_create_mc_questions.rb(the database migration; timestamp will vary), and two files for automated testing,test/fixtures/mc_questions.yml(data for use in the tests) andtest/models/mc_question_test.rb(unit tests for the model class). -
Run the following command to set up the
mc_questionstable in the database and to regenerate thedb/schema.rbfile, which holds the Rails app’s representation of the database.rails db:migrateObserve the changes to the
schema.rbfile and the newmc_questionstable in pgAdmin. -
Add a couple sample questions in the
db/seeds.rbfile as follows:q1 = McQuestion.create!(question: 'What does the M in MVC stand for?', answer: 'Model', distractor_1: 'Media', distractor_2: 'Mode') q2 = McQuestion.create!(question: 'What does the V in MVC stand for?', answer: 'View', distractor_1: 'Verify', distractor_2: 'Validate') q3 = McQuestion.create!(question: 'What does the C in MVC stand for?', answer: 'Controller', distractor_1: 'Create', distractor_2: 'Code')Note that we use the
create!method (with a bang!) to create new database records in theseeds.rbfile. The reason is that the bang version ofcreate(i.e.,create!) throws an exception if something goes wrong, which will, among other things, produce an error message. If the plain oldcreate(with no!) method was used, the command will fail silently, which can be awfully confusing. -
Run the following command to execute the
seeds.rbfile, adding the sample records to the database.rails db:seedObserve the new records in pgAdmin.
➥ Code changeset for this part
2. Retrieving and Viewing Records from the Database
In the first part of this demo, we stored a few questions in the database, so in this part, we will demonstrate how to retrieve and display the questions. In particular, we will demonstrate how to display a single question using a show action and how to display all the questions using an index action. These two actions are semi-standard read (as in the R in CRUD) actions in Rails. There are also semi-standard create, update, and destroy actions that we will cover in future demos.
-
Create a controller for
McQuestionobjects by running therails generate controller ...command as follows:rails g controller McQuestions index showNote that this command generates the file
app/controllers/mc_questions_controller.rb, which contains the classMcQuestionsController. This controller class has two actions (public methods),indexandshow. The command also inserted into theroutes.rbfile a new route for each of these controller actions, and it created two new view files,app/views/mc_questions/index.html.erbandapp/views/mc_questions/show.html.erb. -
Replace the generated routes with standard resources routes for the
McQuestionsController’sindexandshowactions as follows:get 'mc_questions', to: 'mc_questions#index', as: 'mc_questions' # index get 'mc_questions/:id', to: 'mc_questions#show', as: 'mc_question' # showPreviously, we saw parameters passed to the Rails server via POST requests (recall the
paramshash); however, parameters can also be passed via GET requests. In particular, theshowroute has as part of its URI pattern an:idrequest parameter that becomes part of the URL (e.g., http://localhost:3000/mc_questions/125). The:idvalue for a given request can be retrieved fromparams[:id].Additionally, it is possible to pass even more parameters, if needed, by appending them to the end of the URL, as in this example: http://localhost:3000/somepath?keyword1=value1&keyword2=value2&keyword3=value3.
-
Add the standard
respond_toblocks to theshowandindexactions. -
We will also need to add the object(s) to display to the controller actions by using the ActiveRecord query methods for our
McQuestionmodel class. For theshowaction, which displays only one object, we use thefindmethod with anidparameter as follows:question = McQuestion.find(params[:id])For the
indexaction, which displays all the records from that table, we use theallmethod as follows:questions = McQuestion.allYou will also need to pass those variables into the
localshash, so they will be available in the view. -
The
showaction should display all the important attributes of themc_questionobject (i.e., not the timestamps and not theidbecause that is shown in the URL). We could simply set up something like<p>Answer: <%= answer %></p>for each attribute, but let’s make it a little more interesting by showing the question and a set of disabled radio buttons for all the answer choices (the correct answer and distractor(s)). Note that the radio buttons are just for show in this case, and will not be part of a working form.-
Create a
<p>block for the question text as follows:<p><%= question.question %></p> -
Now we need to create the radio button group with all the answers. Let’s make three radio button tags as follows:
<div> <%= radio_button_tag "guess", question.answer, checked = true, disabled: true %> <%= label_tag "guess_#{question.answer}", question.answer %> </div> <div> <%= radio_button_tag "guess", question.distractor_1, checked = false, disabled: true %> <%= label_tag "guess_#{question.distractor_1}", question.distractor_1 %> </div> <div> <%= radio_button_tag "guess", question.distractor_2, checked = false, disabled: true %> <%= label_tag "guess_#{question.distractor_2}", question.distractor_2 %> </div>In the
radio_button_tagoptions, we need to make sure the buttons are all disabled by addingdisabled: true. We also need to be sure that only the correct answer is checked by setting thecheckedoption to be true only for the answer radio button. For the unique label tag IDs, we use string interpolation to execute some ruby code and put it inside the string (e.g.#{question.answer}). -
Coding each radio button, like we did above, works, but it results in a lot of duplicate code. Instead, we can programmatically generate the radio buttons by looping through an array that contains all the choices and creating the radio button inside the loop.
<% choices = [question.answer, question.distractor_1, question.distractor_2] choices.each do |c| %> <div> <%= radio_button_tag "guess", c, checked = c == question.answer, disabled: true %> <%= label_tag "guess_#{c}", c %> </div> <% end %>Setting most of the options is straightforward based on the
cvalue but determining which radio button to check is more complicated. We know thecheckedoption should be true only ifcisquestion.answer, so we can actually setcheckedequal to the the result of determining ifcis equal toquestion.answer. -
Try going to http://localhost:3000/mc_questions/1 to see the show page for one of the questions we added to the database.
-
-
The
indexaction should display some data for each of the records in the associated database table. Often,indexactions will display the database table in an HTMLtableelement, with a row for each record and a column for each of the record attributes; however, I will direct you to pgAdmin’s show-all-records feature for that. In this demo, we will simply display all theMcQuestionobjects on one page in the same way that they were displayed individually by theshowaction. However, each question will be wrapped in its own HTMLdivelement with a uniqueidattribute generated by thedom_idhelper.-
Create a heading for the page as follows:
<h1>Multiple Choice Questions</h1> -
Recall that we added a local
questionsvariable to theindexaction that contains all theMcQuestionobjects in the database. Add code that loops through each question and creates an emptydivelement (for now) with a uniqueidfor each question using thedom_idhelper as follows:<% questions.each do |question| %> <div id="<%= dom_id(question) %>"> </div> <% end %>Navigating to the index page now shows that three
divelements have been created that look like<div id="mc_question_1">where the number is the recordid. -
Each question should be displayed the same as on the
showpage, so we copy theshowpage code into the emptydivwe have, like this:<% questions.each do |question| %> <div id="<%= dom_id(question) %>"> <p><%= question.question %></p> <% choices = [question.answer, question.distractor_1, question.distractor_2] choices.each do |c| %> <div> <%= radio_button_tag "guess", c, checked = c == question.answer, disabled: true %> <%= label_tag "guess_#{c}", c %> </div> <% end %> </div> <% end %> -
If we reload the
indexpage now, we can see that all the questions are displayed, but only one radio button is checked on the entire page. Looking at the code we copied from theshowpage, the radio button tagidis “guess” for all the radio buttons. This was fine for theshowpage, but it now means that the radio buttons for all the questions belong to the same group, and only one can ever be checked at a time. We fix this problem by generating a unique radio button group ID for each question, like this:<%= radio_button_tag "guess_#{question.id}", c, checked = c == question.answer, disabled: true %> <%= label_tag "guess_#{question.id}_#{c}", c %>Now, the group ID should be unique for each question. I also changed the
labeltagidfor consistency.
-
The QuizMe app now provides a way to view an individual multiple-choice question (show) and a way to view all the multiple-choice questions on a single page (index).