CSC/ECE 517 Fall 2023 - E2372. Reimplement QuizQuestion and its child classes: Difference between revisions
Line 155: | Line 155: | ||
In Ruby, <code><<</code> can be used a string concatenation operator. We replaced all instances of using <code>+=</code> to <code><<</code> because we are modifying an existing object <code>html</code> rather than creating a new object. Here's an example from QuizQuestion's <code>view_question_text</code>. | In Ruby, <code><<</code> can be used a string concatenation operator. We replaced all instances of using <code>+=</code> to <code><<</code> because we are modifying an existing object <code>html</code> rather than creating a new object. Here's an example from QuizQuestion's <code>view_question_text</code>. | ||
{| class="wikitable" | {| class="wikitable" " | ||
|- | |- | ||
! Before refactoring !! After refactoring | ! Before refactoring !! After refactoring | ||
Line 207: | Line 207: | ||
|} | |} | ||
=== Testing HTML with Nokogiri === | === Testing HTML with Nokogiri === | ||
<b>Before refactoring:</b> | |||
<pre style ="white-space: pre-wrap;">expect(true_false.edit).to eq('<tr><td><textarea cols="100" name="question[1][txt]" id="question_1_txt"> Test question:</textarea></td></tr><tr><td>Question Weight: <input type="number" name="question_weights[1][txt]" id="question_wt_1_txt" value="1" min="0" /></td></tr><tr><td><input type="radio" name="quiz_question_choices[1][TrueFalse][1][iscorrect]" id="quiz_question_choices_1_TrueFalse_1_iscorrect_True" value="True" checked="checked" />True</td></tr> <tr><td><input type="radio" name="quiz_question_choices[1][TrueFalse][1][iscorrect]" id="quiz_question_choices_1_TrueFalse_1_iscorrect_True" value="False" />False</td></tr>') | |||
</pre> | |||
<b>After refactoring:</b> | |||
<tr><td>Question Weight: <input type="number" name="question_weights[1][txt]" id="question_wt_1_txt" value="1" min="0" /></td></tr> | |||
<tr><td><input type="radio" name="quiz_question_choices[1][TrueFalse][1][iscorrect]" id="quiz_question_choices_1_TrueFalse_1_iscorrect_True" value="True" checked="checked" />True</td></tr> | |||
<tr><td><input type="radio" name="quiz_question_choices[1][TrueFalse][1][iscorrect]" id="quiz_question_choices_1_TrueFalse_1_iscorrect_True" value="False" />False</td></tr>')</pre> | |||
<pre>html = Nokogiri::HTML(true_false.edit) | <pre>html = Nokogiri::HTML(true_false.edit) | ||
# Test for presence of a text area for the question text | |||
expect(html.css('textarea[name="question[1][txt]"]')).not_to be_empty | |||
# Test for presence of an input for the question weight | |||
expect(html.css('input[name="question_weights[1][txt]"]')).not_to be_empty | |||
# Test for the correct number of choices | |||
expect(html.css('input[type="radio"][name^="quiz_question_choices[1][TrueFalse]"]').size).to eq(2)</pre> | |||
=== Adding <code>multiple_choice_radio_spec.rb</code> === | === Adding <code>multiple_choice_radio_spec.rb</code> === | ||
Revision as of 21:05, 4 December 2023
Objectives
There are four methods in QuizQuestion super class. edit, view_question_text, view_completed_question, and complete. These 4 (model) methods will return different html strings depends on the question type.
edit
: What to display` - if an instructor (etc.) is creating or editing a questionnaire (questionnaires_controller.rb)view_question_text
: What to display if an instructor (etc.) is viewing a questionnaire (questionnaires_controller.rb)complete
: What to display - if a student is filling out a questionnaire (response_controller.rb)view_completed_question
: What to display if a student is viewing a filled-out questionnaire (response_controller.rb)
Our goals are
- Make a complete UML diagram for these classes with their attributes, methods, and class methods.
- Check all implementations of instance methods, extract duplicated codes, and put them into the highest-level class.
- Thoroughly test the QuizQuestion class
Overview of the Classes
quiz_question.rb
QuizQuestion is a base class used to represent a question in a quiz. It inherits from the Question class. This class is associated with QuizQuestionChoice objects through a has_many relationship, indicating that a QuizQuestion can have multiple choices. It handles the creation of HTML for different views related to a question, such as editing, completing, and viewing the question text.
Methods:
edit
: to generate the HTML form for editing a question,complete
: used for completing the question in the quizview_question_text
: to display the question and its choices)view_completed_question
: abstract method used after quiz has been submitted, andisvalid
: to validate the question, especially ensuring it has text
multiple_choice_radio.rb
This class extends QuizQuestion and represents a multiple-choice question where the participant can select only one answer from a set of radio buttons.
Methods:
edit
: overridden to add HTML for a set of radio buttons that allows the user to select one choice as the correct answer; also includes text inputs for the choices' textcomplete method
: renders HTML for the user to complete this type of question in a quiz interfaceview_completed_question
: displays the correct answer, the user's answer, and visual feedback on whether the user's answer was correctisvalid
: extended to ensure that not only does each choice have text, but also that exactly one correct answer is selected
multiple_choice_checkbox.rb
The MultipleChoiceCheckbox class is a subclass of QuizQuestion and represents a multiple-choice question where the participant can select multiple answers through checkboxes.
Methods:
edit
: add HTML input elements for checkboxes, allowing multiple correct answers to be selected, along with text inputs for entering the choices.complete
: allows users to complete the question, displaying checkboxes for them to select their answersview_completed_question
: shows correct answers and whether the user's selections were correctisvalid
: validates the question, ensures there are multiple correct answers if it's a checkbox question.
true_false.rb
This class also extends QuizQuestion and is specialized for true/false questions.
Methods:
edit
: includes HTML radio buttons specifically for 'True' and 'False' optionscomplete
: render the true/false question for the user to answerview_completed_question
: displays the correct answer and the user's answerisvalid
: checks to ensure that the question has text and that a correct answer is designated
Our Solution
- We implemented
isvalid(choice_info)
in QuizQuestion.
def isvalid(choice_info) @valid = 'valid' @valid = 'Please make sure all questions have text' if txt == '' @valid end
- We factored out duplicate code in
edit
from the subclasses and placed it into the superclass QuizQuestion. Nowedit
in the subclasses inherit partially from QuizQuestion.
edit
in QuizQuestion
def edit @quiz_question_choices = QuizQuestionChoice.where(question_id: id) @html = '<tr><td>' @html += '<textarea cols="100" name="question[' + id.to_s + '][txt]" ' @html += 'id="question_' + id.to_s + '_txt">' + txt + '</textarea>' @html += '</td></tr>' @html += '<tr><td>' @html += 'Question Weight: ' @html += '<input type="number" name="question_weights[' + id.to_s + '][txt]" ' @html += 'id="question_wt_' + id.to_s + '_txt" ' @html += 'value="' + weight.to_s + '" min="0" />' @html += '</td></tr>' end
edit
in MultipleChoiceCheckbox
def edit super # for i in 0..3 [0, 1, 2, 3].each do |i| @html += '<tr><td>' @html += '<input type="hidden" name="quiz_question_choices[' + id.to_s + '][MultipleChoiceCheckbox][' + (i + 1).to_s + '][iscorrect]" ' @html += 'id="quiz_question_choices_' + id.to_s + '_MultipleChoiceCheckbox_' + (i + 1).to_s + '_iscorrect" value="0" />' @html += '<input type="checkbox" name="quiz_question_choices[' + id.to_s + '][MultipleChoiceCheckbox][' + (i + 1).to_s + '][iscorrect]" ' @html += 'id="quiz_question_choices_' + id.to_s + '_MultipleChoiceCheckbox_' + (i + 1).to_s + '_iscorrect" value="1" ' @html += 'checked="checked" ' if @quiz_question_choices[i].iscorrect @html += '/>' @html += '<input type="text" name="quiz_question_choices[' + id.to_s + '][MultipleChoiceCheckbox][' + (i + 1).to_s + '][txt]" ' @html += 'id="quiz_question_choices_' + id.to_s + '_MultipleChoiceCheckbox_' + (i + 1).to_s + '_txt" ' @html += 'value="' + @quiz_question_choices[i].txt + '" size="40" />' @html += '</td></tr>' end @html.html_safe # safe_join(@html) end
- We reimplemented QuizQuestion class and child classes methods to use
@html
(an instance variable) instead ofhtml
.
Final Phase Planning
- Further refactor quiz_question.rb and subclasses
- Currently, quiz_question.rb is a subclass of question.rb, however it does not follow the Liskov Substitution Principle because the method isvalid is not present in the superclass. By refactoring isvalid into the superclass, the entire Question hierarchy can be made more maintainable.
- Most methods of quiz_question.rb contain hard-coded HTML, which makes the code inflexible and prone to breaking, and also violates SOP. We plan to refactor the HTML building logic out of the methods to improve readability and maintainability. This would also make testing easier.
UML Diagram
Final Phase Changes
Refactoring QuizQuestion and its child classes
1. Extract Method
We applied the extract method to make the code more readable.
For example, in the edit
method in MultipleChoiceRadio, we created new methods and copied the relevant code fragments from edit
into the new methods.
def edit @quiz_question_choices = QuizQuestionChoice.where(question_id: id) @html = create_html_content end
Private methods:
def create_html_content html_content = "" html_content << create_textarea_row html_content << create_weight_input_row html_content end def create_textarea_row "<tr><td><textarea cols='100' name='question[#{id}][txt]' id='question_#{id}_txt'>#{txt}</textarea></td></tr>" end def create_weight_input_row "<tr><td>Question Weight: <input type='number' name='question_weights[#{id}][txt]' id='question_wt_#{id}_txt' value='#{weight}' min='0' /></td></tr>" end
2. Replacing +=
with <<
In Ruby, <<
can be used a string concatenation operator. We replaced all instances of using +=
to <<
because we are modifying an existing object html
rather than creating a new object. Here's an example from QuizQuestion's view_question_text
.
Before refactoring | After refactoring |
---|---|
def view_question_text @html = '<b>' + txt + '</b><br />' @html += 'Question Type: ' + type + '<br />' @html += 'Question Weight: ' + weight.to_s + '<br />' if quiz_question_choices quiz_question_choices.each do |choices| @html += if choices.iscorrect? ' - <b>' + choices.txt + '</b><br /> ' else ' - ' + choices.txt + '<br /> ' end end @html += '<br />' end @html.html_safe end |
def view_question_text @html = "<b>#{txt}</b><br />" @html << "Question Type: #{type} <br />" @html << "Question Weight:#{weight.to_s} <br />" @html << create_choices if quiz_question_choices.present? @html.html_safe end Private methods: def create_choices choices_html = "" quiz_question_choices.each do |choice| choices_html << choice_html(choice) end choices_html << "<br />" choices_html end def choice_html(choice) if choice.iscorrect? " - <b>#{choice.txt}</b><br /> " else " - #{choice.txt}<br /> " end end def all_choices_have_text?(choice_info) choice_info.all? { |_idx, value| value[:txt].present? } end |
Testing HTML with Nokogiri
Before refactoring:
expect(true_false.edit).to eq('<tr><td><textarea cols="100" name="question[1][txt]" id="question_1_txt"> Test question:</textarea></td></tr><tr><td>Question Weight: <input type="number" name="question_weights[1][txt]" id="question_wt_1_txt" value="1" min="0" /></td></tr><tr><td><input type="radio" name="quiz_question_choices[1][TrueFalse][1][iscorrect]" id="quiz_question_choices_1_TrueFalse_1_iscorrect_True" value="True" checked="checked" />True</td></tr> <tr><td><input type="radio" name="quiz_question_choices[1][TrueFalse][1][iscorrect]" id="quiz_question_choices_1_TrueFalse_1_iscorrect_True" value="False" />False</td></tr>')
After refactoring:
html = Nokogiri::HTML(true_false.edit) # Test for presence of a text area for the question text expect(html.css('textarea[name="question[1][txt]"]')).not_to be_empty # Test for presence of an input for the question weight expect(html.css('input[name="question_weights[1][txt]"]')).not_to be_empty # Test for the correct number of choices expect(html.css('input[type="radio"][name^="quiz_question_choices[1][TrueFalse]"]').size).to eq(2)
Adding multiple_choice_radio_spec.rb
An explaination of why isvalid cannot be added to QuizQuestion
's superclass Question
Testing
- Most existing Rspec tests for
quiz_question.rb
and child classes test for exact strings that match the HTML. These tests will break if anything about the classes are changed, therefore they are not robust tests. The tests should be refactored to test the logic of these methods rather than their exact outputs.
- In general, create additional tests after refactoring to verify that the refactored code work as intended. This includes functional testing to incorporate any additional helper classes created during the refactoring.
Relevant Links
Github repository: https://github.com/opheliasin/expertiza
Pull request: https://github.com/expertiza/expertiza/pull/2682
Team
Mentor
Aditi Vakeel
Members
Nathan Sudduth (ncsuddut@ncsu.edu)
Ophelia Sin (oysin@ncsu.edu)
Yi Chen (ychen282@ncsu.edu)