CSC/ECE 517 Spring 2016/Refactor different question types from quiz feature

From Expertiza_Wiki
Jump to navigation Jump to search

This wiki page is for the description of changes made under E1602 OSS assignment for Spring 2016, CSC/ECE 517.

Introduction

Background

Expertiza is an open source project based on Ruby on Rails framework. Expertiza has a quizzing feature which allow student authors to create quiz questions and test the peer reviewers. The idea behind this is, if a reviewer can answer the quiz questions which were created by the author correctly, we assume that the reviewer has read the artifact carefully enough and thereby we trust the peer-review.

Current Implementation and Problems

  • Following three different types of questions are supported in quizzing feature of Expertiza
  1. Multiple Choice Radio
  2. Multiple Choice Checkbox
  3. True/False
  • The current implementation of Quizzing feature is not consistent with the current questions and questionnaires. Since Quiz questionnaire is one sub type of questionnaire, so it should follow the design of other type of questionnaires. But, the current implementation is inconsistent with other questionnaires.
  • In order to create a quiz, the existing code is written in the views which has a series of if-then structures to implement the functionality of each different question type. This kind of implementation makes the code slower and violates the principles of MVC architecture.
  • Current HTML code doesn't looks like source code.
  • Code is repeated for different question types.
  • Proper Ruby naming conventions are not followed

Changes Implemented

  • All logic moved to model. Four separate methods - edit, view_question_text, complete and view_completed_question in order to generate the same HTML for each of the question types. We used polymorphism by declaring all these methods in QuizQuestion which inherits from Question and has sub-types MultipleChoiceRadio, MultipleChoiceCheckbox, and TrueFalse.
  • When questionnaire author creates a quiz, edit is called. To call this on UI, you can login as a student, click 'create quiz' / 'edit quiz' on 'Your Works' page. The edit method is now called from /view/questionnaires/_quiz_question.html.erb
  • When questions are viewed by the author or the instructor, view_question_text method is called. To call this on UI, you can either login as a student, click 'view quiz' on 'Your works' page or login as an instructor, find an assignment with quizzing feature and click the icon 'view quiz questions'. The 'view_question_text' calls view/questionnaires/view.html.erb
  • When the quiz takers tries to take the quiz, complete is called. To call this on UI, you can login as a quiz taker, request a quiz to take on “Take quizzes” page, then click “begin”. This method is now called on view/student_quizzes/take_quiz.
  • When the quiz taker finished taking a quiz and wants to view the quiz result again, view_completed_question is called. To call this on UI, you can login as a quiz taker, find a finished quiz and click “view”. This method is called on view/student_quizzes/finished_quiz.
  • Using polymorphism and DRY principles, code repetition has been virtually eliminated.

Refactoring view_question_text

The view_question_text method is called when the author of the quiz or the instructor views the quiz. The method is responsible for generating the HTML to display the question along with each of its choices. The correct choice(s) should be bolded. Prior to refactoring, this functionality was located in app/views/student_quizzes/review_questions.html.erb:

<%questions = questionnaire.questions%>
      <% questions.each do |question| %>
        <b> <%= question.txt %><br></b>
        <% question_type = question.get_formatted_question_type%>
        Question Type: <%= question_type %><br>

        <% if question_type == 'True/False' %>
          Correct Answer: <%= (QuizQuestionChoice.where(question_id: question.id, iscorrect:1).first.txt) %><br><br>
        <% elsif question_type == 'Multiple Choice - Radio' %>
          <% correct_answer = '' %>
          Answer Options:<br>
          <% QuizQuestionChoice.where(question_id: question.id).each do |choice| %>
            <% if choice.iscorrect == true  %>
                <B><%= choice.txt %></B><br>
            <%else%>
                <%= choice.txt %><br>
            <% end %>

          <% end %>
          <br><br>
        <% elsif question_type == 'Multiple Choice - Checked' %>
          Answer Options:<br>
          <% QuizQuestionChoice.where(question_id: question.id).each do |choice| %>
            <% if choice.iscorrect == true  %>
                <B><%= choice.txt %></B><br>
            <%else%>
                <%= choice.txt %><br>
            <% end %>
          <% end %>
          <br><br>
        <% end %>
      <% end %><hr>

This logic has been moved to app/models/quiz_question.rb, so now the view simply needs to call the method:

<%questions = questionnaire.questions%>
      <%questions.each do |question| %>

        <%if question.is_a? Question%>
          <%=question.view_question_text.html_safe%>
        <%end%>
        
      <% end %><hr>

Unit tests were added to spec/models/quiz_question_spec.rb covering all three types of questions to ensure that the correct HTML is generated.

Refactoring edit

The edit method is called when the author creates or edits a quiz. It makes every element of the quiz editable. Prior to refactoring, the HTML was in app/views/questionnaires/_quiz_questionnaire.html.erb:

<% if @questionnaire.type == 'QuizQuestionnaire' %>        
  <!--handle a questionnaire differently if it is a quiz-->
    <table id="questions_table">
      <% if $disp_flag != 1 %>
        <tr>
          <td align=left width=300>Questions:</td>
          <%  if @questionnaire.type != 'QuizQuestionnaire' %><td align=left>weight</td><%end%>

        </tr>
      <% end %>

      <% for @question in @questionnaire.questions %>
          <% questionnum=@question.id %>
        <tr>
          <% if $disp_flag != 1 %>
            <td > <%= text_area "question[]", 'txt', :cols=>100 %></td>
          <% end %>
          <%  @quiz_question_choices = QuizQuestionChoice.where(question_id: @question.id) %>

        </tr>
        <% i=1 %>

        <% for @quiz_question_choice in @quiz_question_choices %>
          <tr>
          <td>
               
              <% if @question.type=="MultipleChoiceCheckbox" %>
                  <%= hidden_field_tag("quiz_question_choices[#{questionnum}][MultipleChoiceCheckbox][#{i}][iscorrect]",'0') %>
                  <%= check_box_tag("quiz_question_choices[#{questionnum}][MultipleChoiceCheckbox][#{i}][iscorrect]",'1', @quiz_question_choice.iscorrect) %>
                   <%= text_field_tag "quiz_question_choices[#{questionnum}][MultipleChoiceCheckbox][#{i}][txt]", @quiz_question_choice.txt, :size=>40 %>

              <% end %>

              <%if @question.type=="MultipleChoiceRadio" %>
                <%= radio_button_tag("quiz_question_choices[#{questionnum}][MultipleChoiceRadio][correctindex]", "#{i}", @quiz_question_choice.iscorrect)%>
                 <%= text_field_tag "quiz_question_choices[#{questionnum}][MultipleChoiceRadio][#{i}][txt]", @quiz_question_choice.txt, :size=>40 %>
              <% end %>

              <%if @question.type=="TrueFalse" %>
                <% if @quiz_question_choice.txt=="True"%>
                  <%= radio_button_tag("quiz_question_choices[#{questionnum}][TrueFalse][1][iscorrect]", 'True', @quiz_question_choice.iscorrect) %>   True
                <%end%>

                <% if @quiz_question_choice.txt=="False"%>
                    <%= radio_button_tag("quiz_question_choices[#{questionnum}][TrueFalse][1][iscorrect]", 'False', @quiz_question_choice.iscorrect) %>   False
                <%end%>
              <% end %>
          </td>
          </tr>
          <% i+=1 %>
        <%end %>

      <% end %>
    </table> 
  <% end %>

After refactoring, the same file looked like:

<% if @questionnaire.type == 'QuizQuestionnaire' %>
  <!--handle a questionnaire differently if it is a quiz-->
    <table id="questions_table">
      <% if $disp_flag != 1 %>
        <tr>
          <td align=left width=300>Questions:</td>
          <%  if @questionnaire.type != 'QuizQuestionnaire' %><td align=left>weight</td><%end%>

        </tr>
      <% end %>

      <% for @question in @questionnaire.questions %>
      <% i=1 %>
         <%= @question.edit(i) %>
      <% end %>
    </table>
  <% end %>

Refactoring complete

Logic is written in multiple_choice_checkbox model for multiple choice checkbox question type. Similarly, same process is followed for remaining question types. To begin a quiz, complete is called from the model.

    @question = self
    html = ""
    html += label_tag("#{question.id}", question.txt) +"
" quiz_question_choices = QuizQuestionChoice.where(question_id: @question.id) quiz_question_choices.each do |choice| if answer=="view" html += check_box_tag ("#{question.id}[]", "#{choice.txt}", choice.iscorrect) html += label_tag("#{choice.txt}", choice.txt) + "
" end if answer=="take" html += check_box_tag ("#{question.id}[]", "#{choice.txt}") html += label_tag("#{choice.txt}", choice.txt) + "
" end html.html_safe end end

Refactoring view_completed_question

The logic for viewing completed question and their answers is written in app/views/student_quizzes/finished_quiz.html.erb using a redundant if-elsif structure as shown below.

<% if question_type.eql? 'MultipleChoiceRadio' %>
   <% QuizQuestionChoice.where(question_id: question.id).each do |answer|  %>
     <% if(answer.iscorrect) %>
       <%= p answer.txt  %> -- Correct 
<%else%> <%= p answer.txt  %>
<% end %> <% end %>
<%user_answer=Answer.where(response_id: @response.id, question_id: question.id).first%> Your answer is: <%= user_answer.comments  %> <%if user_answer.answer==1%> <img src="/assets/Check-icon.png"/> <%else%> <img src="/assets/delete_icon.png"/> <%end%>

<% elsif question_type.eql? 'MultipleChoiceCheckbox' %> ... <% elsif(question_type == 'TrueFalse') %> ...

The app/views/student_quizzes/finished_quiz.html.erb was refactored by removing the logic from the view and making it clean. We used a single construct which determines question type and answer type by calling view_completed_question method declared in quiz class as shown below.

<%user_answer=Answer.where(response_id: @response.id, question_id:  question.id)%>
Question <%= i %>: <%= label_tag "#{question.id}", question.txt %> 
<%= question.view_completed_question(i,user_answer) %> <% i += 1 %>

Similarly, logic was written in models for each of the question types. Following code snippet shows view_completed_question for true/false question type. def view_completed_question(count, answer)

    @question = self
    html=""
    html+= "Correct Answer is: "
    html+= QuizQuestionChoice.where(question_id: @question.id,iscorrect: 1).first.txt
    html+= "
" html+= "Your answer is: " html+= answer.first.coments + "" if(answer.first.answer == 1) html+= "<img src=/assets/Check-icon.png/>" else html+= "<img src=/assets/delete_icon.png/>" end html+= "

" end

Remove irrelevant comments from the student_quizzes_controller The student_quizzes_controller has a few comments which are irrelevant and make the method bulky. These could be safely removed from the code.

Unit Testing

We added unit tests in spec/models/quiz_question_spec.rb which check the generated HTML for each of the refactored methods. by We used context blocks to incorporate tests for each type of question. Here is an example of the tests for view_question_text:

describe "view_question_text" do
    context "when the question is a multiple-choice radio" do
      it "contains the text of the question" do
        expect(@radio_question.view_question_text.html_safe).to include("In which city is NCSU located?")
      end

      it "contains each of the choices" do
        expect(@radio_question.view_question_text.html_safe).to include("Raleigh")
        expect(@radio_question.view_question_text.html_safe).to include("Atlanta")
        expect(@radio_question.view_question_text.html_safe).to include("Charlotte")
        expect(@radio_question.view_question_text.html_safe).to include("North Carolina")
      end

      it "bolds the correct answer" do
        expect(@radio_question.view_question_text.html_safe).to include("<b>Raleigh</b>")
      end

      it "does not bold incorrect answers" do
        expect(@radio_question.view_question_text.html_safe).not_to include("<b>Atlanta</b>")
        expect(@radio_question.view_question_text.html_safe).not_to include("<b>Charlotte</b>")
        expect(@radio_question.view_question_text.html_safe).not_to include("<b>North Carolina</b>")
      end
    end

    context "when the question is a multiple-choice checkbox" do
      it "contains the text of the question" do
        expect(@checkbox_question.view_question_text.html_safe).to include("Which of the following are colors?")
      end

      it "contains each of the choices" do
        expect(@checkbox_question.view_question_text.html_safe).to include("Paint")
        expect(@checkbox_question.view_question_text.html_safe).to include("Blue")
        expect(@checkbox_question.view_question_text.html_safe).to include("Car")
        expect(@checkbox_question.view_question_text.html_safe).to include("Red")
      end

      it "bolds the correct answers" do
        expect(@checkbox_question.view_question_text.html_safe).to include("<b>Blue</b>")
        expect(@checkbox_question.view_question_text.html_safe).to include("<b>Red</b>")
      end

      it "does not bold incorrect answers" do
        expect(@checkbox_question.view_question_text.html_safe).not_to include("<b>Paint</b>")
        expect(@checkbox_question.view_question_text.html_safe).not_to include("<b>Car</b>")
      end
    end

    context "when the question is true/false" do
      it "contains the text of the question" do
        expect(@true_false_question.view_question_text.html_safe).to include("2+2=4")
      end

      it "contains each of the choices" do
        expect(@true_false_question.view_question_text.html_safe).to include("True")
        expect(@true_false_question.view_question_text.html_safe).to include("False")
      end

      it "bolds the correct answer" do
        expect(@true_false_question.view_question_text.html_safe).to include("<b>True</b>")
      end

      it "does not bold the incorrect answer" do
        expect(@true_false_question.view_question_text.html_safe).not_to include("<b>False</b>")
      end
    end
  end

UI Testing

You may want to use the following information to make UI testing easier if you are using the link in the Expertiza submission:

Assignment: Quiz Assignment
Instructor: super_administrator2
Students: student15, student16, student17, student18, student19, student20

If you are using the link in the Expertiza submission, you do not need a password. We made this change to facilitate testing. The committed code does not make any change for authenticating users.

Since this project involved code refactoring, no new functionality was added. But here are tests for each method to verify they are working correctly.

view_question_text

  1. Login as a student.
  2. Select an assignment that has quizzing enabled (e.g. Quiz Assignment).
  3. Select "Your Work"
  4. If you haven't created a quiz yet, go ahead and create one by clicking the "Create Quiz" link.
  5. Select "View quiz".
  6. You should see each question, followed by the choices. The correct choice(s) will be bolded.

edit

  1. Login as a student.
  2. Select an assignment that has quizzing enabled (e.g. Quiz Assignment).
  3. Select "Your Work"
  4. If you haven't created a quiz yet, go ahead and create one by clicking the "Create Quiz" link.
  5. Select "Edit quiz".
  6. You should see each question, followed by the choices, each in an editable text field. The correct choice(s) will be checked.

complete

  1. Login as a student.
  2. Select an assignment that has quizzing enabled (e.g. Quiz Assignment).
  3. Select "Your Work"
  4. If you haven't created a quiz yet, go ahead and create one by clicking the "Create Quiz" link.
  5. Select "Take Quizzes".
  6. Click 'Begin'.
  7. You should see each question, followed by the choices. You can record your answers for each questions and then click 'submit'. Your answers will be saved.

view_completed_question

  1. Login as a student.
  2. Select an assignment that has quizzing enabled (e.g. Quiz Assignment).
  3. Select "Your Work"
  4. If you haven't created/taken a quiz yet, go ahead and create one by clicking the "Create Quiz" link and take the quiz.
  5. Find a finished quiz and click on 'View'.
  6. You should see each question, followed by the choices. The correct choice(s) will be bolded and your recorded answer will be shown.

N.B. The VCL image sometimes may take a longer time to load. But, it's up and running perfectly.