Forms That Handle One-to-Many Associations
In this demonstration, I will show how incorporate an association into the basic Rails resource pages and actions (i.e., index
, show
, new
/create
, edit
/update
, and destroy
). We will continue to build upon the QuizMe project from the previous demos.
Recall from Figure 1 that our association now specifies that McQuestion
objects belong to a particular parent Quiz
object.
Figure 1. Model class design diagram showing the one-to-many association between Quiz
and McQuestion
. As per the association, each Quiz
object has many McQuestion
objects, and each McQuestion
object belongs to one Quiz
object.
As a consequence, several changes to our Rails resource pages and actions are necessitated. These changes will involve the following main tasks:
- Update the
show
page forQuiz
to display associatedMcQuestion
objects, as depicted in Figure 2. - Generate a
QuizMcQuestionsController
to use for actions onMcQuestion
records that require the ID of the parentQuiz
to be included in their routes. - Move the
index
controller actions forMcQuestion
records into the newQuizMcQuestionsController
class with a new route that includes theQuiz
ID in the URI. - Move the
new
andcreate
controller actions forMcQuestion
records into the newQuizMcQuestionsController
class with new routes that include theQuiz
ID in their URIs. - Update the existing
update
anddestroy
controller actions forMcQuestion
records so that they redirect to the parentQuiz
objectโsshow
page.
![Screenshot of a web browser](/rails-demos-n-deets-2020/resources/quiz_show_with_questions.png)
Figure 2. Updated show
page for Quiz
records that now has a โQuestionsโ subsection that displays the associated McQuestion
objects.
1. Displaying Associated McQuestion
Records on the show
Page for Quiz
Records
On the show
page for a Quiz
object, display the questions associated with that quiz by adding HTML.ERB code to the show.html.erb
, like this:
<h2>Questions</h2>
<% quiz.mc_questions.each do |question| %>
<div id="<%= dom_id(question) %>">
<p>
<%= question.question %>
<%= link_to '๐', mc_question_path(question) %>
<%= link_to '๐', edit_mc_question_path(question) %>
<%= link_to '๐', mc_question_path(question), method: :delete %>
</p>
<%
choices = [question.answer, question.distractor_1]
choices << question.distractor_2 if !question.distractor_2.blank?
choices.each do |c|
%>
<div>
<%= radio_button_tag "guess_#{question.id}", c, checked = c == question.answer, disabled: true %>
<%= label_tag "guess_#{question.id}_#{c}", c %>
</div>
<% end %>
</div>
<% end %>
Confirm that this code works correctly by running the app, opening the index
page for Quiz
records, and navigating to the show
page for each Quiz
record. The show
pages should now include a โQuestionsโ subsection, as depicted in Figure 2.
2. Generating a QuizMcQuestionsController
Class
Create a new controller QuizMcQuestionsController
using this command:
rails g controller QuizMcQuestions
We will use this controller class, QuizMcQuestionsController
, to handle HTTP requests that primarily act upon McQuestion
records, include a parent Quiz
ID in the resource path.
Confirm that the file app/controllers/quiz_mc_questions_controller.rb
was generated correctly.
3. Moving the index
Action from McQuestionsController
to QuizMcQuestionsController
Replace the existing McQuestion
route for index
with a nested route, like this:
# get 'mc_questions', to: 'mc_questions#index', as: 'mc_questions' # index
get 'quizzes/:id/mc_questions', to: 'quiz_mc_questions#index', as: 'quiz_mc_questions' # nested index
Note that this route requires the parent Quiz
ID in the resource path. The index
route needs the parent ID, because it no longer makes sense to display all McQuestion
objects, rather, we will group them by their parent Quiz
object.
Comment out (or delete) the existing index
action in McQuestionsController
.
Add a new index
action to QuizMcQuestionsController
, like this:
def index
quiz = Quiz.includes(:mc_questions).find(params[:id])
respond_to do |format|
format.html { render :index, locals: { quiz: quiz, questions: quiz.mc_questions } }
end
end
The includes
method helps minimize the number of database queries by specifying the associations that need to be loaded (see the N+1 Queries Problem).
Move the index.html.erb
view file from app/views/mc_questions
to app/views/quiz_mc_questions
.
Confirm that these changes work correctly by running the app and opening the URL http://localhost:3000/quizzes/1/mc_questions for the first Quiz
record and http://localhost:3000/quizzes/1/mc_questions for the second Quiz
record.
4. Moving new
and create
Actions from McQuestionsController
to QuizMcQuestionsController
Replace the existing McQuestion
routes for new
, and create
with nested routes, like this:
# get 'mc_questions/new', to: 'mc_questions#new', as: 'new_mc_question' # new
get 'quizzes/:id/mc_questions/new', to: 'quiz_mc_questions#new', as: 'new_quiz_mc_question' # nested new
# post 'mc_questions', to: 'mc_questions#create' # create
post 'quizzes/:id/mc_questions', to: 'quiz_mc_questions#create' # nested create
Note that these routes both require the parent Quiz
ID in the resource path. The new
and create
routes need the parent Quiz
ID, so the create
controller action knows which Quiz
object has the new McQuestion
object.
Comment out (or delete) the existing new
and create
actions in McQuestionsController
.
Add a new new
action to QuizMcQuestionsController
, like this:
def new
quiz = Quiz.find(params[:id])
respond_to do |format|
format.html { render :new, locals: { quiz: quiz, question: quiz.mc_questions.build } }
end
end
The call to build
allocates in memory a new empty McQuestion
object that is associated with the quiz
; however, the McQuestion
object is not yet saved to the database (and thus, for example, has an id
that is nil
).
Move the new.html.erb
view file from app/views/mc_questions
to app/views/quiz_mc_questions
.
In new.html.erb
, change the url
argument for form_with
, like this:
<%= form_with model: question, url: quiz_mc_questions_path(quiz), method: :post, local: true, scope: :mc_question do |form| %>
Add a new create
action to QuizMcQuestionsController
, like this::
def create
# find the quiz to which the new question will be added
quiz = Quiz.find(params[:id])
# allocate a new question associated with the quiz
question = quiz.mc_questions.build(params.require(:mc_question).permit(:question, :answer, :distractor_1, :distractor_2))
# respond_to block
respond_to do |format|
format.html do
if question.save
# success message
flash[:success] = "Question saved successfully"
# redirect to index
redirect_to quiz_mc_questions_url(quiz)
else
# error message
flash.now[:error] = "Error: Question could not be saved"
# render new
render :new, locals: { quiz: quiz, question: question }
end
end
end
end
Update the โNew Question
โ link in quiz_mc_questions/index.html.erb
and add a โNew Question
โ link to quizzes/show.html.erb
(as per Figure 1), like this:
<%= link_to 'New Question', new_quiz_mc_question_path(quiz) %>
Confirm that these changes work correctly by resetting the database, running the app, navigating the various show
pages for Quiz
records, and using the โNew Question
โ link to add new McQuestion
records.
5. Updating the McQuestion
update
and destroy
Actions to Use the Parent Quiz
Modify the update
action in McQuestionsController
such that, on a successful save, the browser is redirected to the show
page of the parent Quiz
object, like this:
def update
# load existing object again from URL param
question = McQuestion.find(params[:id])
# respond_to block
respond_to do |format|
format.html do
if question.update(params.require(:mc_question).permit(:question, :answer, :distractor_1, :distractor_2))
# success message
flash[:success] = 'Question updated successfully'
# redirect to index
redirect_to quiz_url(question.quiz)
else
# error message
flash.now[:error] = 'Error: Question could not be updated'
# render edit
render :edit, locals: { question: question }
end
end
end
end
Change the destroy
action in McQuestionsController
such that, after the record is deleted, the browser is redirected to the show
page of the parent Quiz
object, like this:
def destroy
# load existing object again from URL param
question = McQuestion.find(params[:id])
# destroy object
question.destroy
# respond_to block
respond_to do |format|
format.html do
# success message
flash[:success] = 'Question removed successfully'
# redirect to index
redirect_to quiz_url(question.quiz)
end
end
end
Confirm that these changes work correctly by resetting the database, running the app, navigating the various show
pages for Quiz
records, and using the โ๐
โ (edit) and โ๐
โ (delete) links to update and delete some McQuestion
records for each Quiz
.
Having successfully completed the above tasks, the QuizMe app now provides users with a full set of features for CRUDing quizzes and questions that properly handle the association links between quizzes and questions.