E1911 Refactor Criterion: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
No edit summary
 
(47 intermediate revisions by 3 users not shown)
Line 1: Line 1:
==E1553. Refactoring the Versions Controller==
=E1911 Refactoring criterion.rb=


This page provides a description of the Expertiza based OSS project.  
This page provides a description of the Expertiza based OSS project.  
Line 7: Line 7:




===About Expertiza===
==About Expertiza==


[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.
[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.


===Problem Statement===
==Problem Statement==
The following tasks were accomplished in this project:
The bulk of the code in criterion is HTML. It is the violation of MVC architecture - Model should not concern itself with how the data is displayed. This code needs to be moved to a partial file, and the partial file needs to be called in all appropriate places which call the criterion’s model methods. Once the logic for the view is moved out of the model, the model should only be left with business logic. This business logic code can also be refactored. Plus there are virtually no comments, properly comment the code.


* Improved the clarity of code by improving the variable and parameter names.
==About Criterion.rb==
* Long character strings were taken and given appropriate names.
Criterion is one of the types of questions that can be added to a questionnaire. There are many other types of question objects like dropdown that have the implementation of similar methods. As of now, the criterion model holds 4 methods that are called from different places in the application. Each of these methods is storing a string value which contains the necessary HTML lines to display the required functionalities to the user. This string value is then returned to the calling methods from the respective views. This HTML string is then entered wherever necessary. This is ideally the exact function of a partial, and this page needs to be heavily reformatted to use partials instead of returning a string containing html code.
* Handled pagination by a separate helper module, which can be used by multiple controllers.
* Implemented action_allowed for access_control to prevent unauthorized access of methods.
* Prevented displaying of all versions for all users and tables when a user views the index page.
* Added missing CRUD methods to Versions Controller
* Added RSPEC testcases for testing changes done in Versions Controller


===About Versions Controller===
===The pull request===
This class manages different versions of reviews. If a reviewer reviews a submission, and after that, the author revises the submission, the next time the reviewer does a review (s)he will create a new version. Sometimes it’s necessary to find the current version of a review; sometimes it’s necessary to find all versions. Similarly, a user may want to delete the current version of a review, or all versions of a review. Pagination of versions helps the user to view a subset of versions at a time. Considering the huge number of versions in the system, it is very useful to have a pagination mechanism and a filtering mechanism which can be applied on the whole set of versions. The idea is to display the versions in an ordered, comprehensible and logical manner. In Expertiza the gem ‘will_paginate’ is used to achieve pagination.
https://github.com/expertiza/expertiza/pull/1407


===Current Implementation===
===The edit method===
This method is one of the several other methods in other files like criterion.rb which gets called when the type of the question being created in the respective model. In the case of this problem, which deals with criterion type questions, the edit method defined in criterion.rb is called when the question.type is equal to "Criterion". This method allows for a user to create the criterion question and enter the necessary details. This method is called only from two view files - one for creating (_questionnaire.html.erb partial file) and another for editing (edit.html.erb). Both these files are in questionnaires view. Verified this using both RubyMine and the grep command. Another interesting thing to note is the use of "self." to get the attributes associated with question object. When we move this code to a partial, we need to change this in the erb file. The outcome of this method's refactoring will completely remove this method from the model file criterion.rb as there is no business logic involved here.


<pre>
def edit(_count)
    html = '<td align="center"><a rel="nofollow" data-method="delete" href="/questions/' + self.id.to_s + '">Remove</a></td>'


=====Functionality=====
    html += '<td><input size="6" value="' + self.seq.to_s + '" name="question[' + self.id.to_s + '][seq]"'
* Any user irrespective of his/ her privileges can view all the versions.
    html += ' id="question_' + self.id.to_s + '_seq" type="text"></td>'
::The versions which a particular user can view should be restricted based on the privileges of the user. For instance, only a user with Administrator privileges should be able to view all the versions in the system. However, that is not the case now. Every user can view all the versions irrespective of whether the user is a student or an administrator.
* Any user can delete any version
::The versions which a particular user can delete should be restricted based on the privileges of the user. For instance, a student should not be allowed to delete any version. According to the current implementation any user can delete any version in the system.
* Filtering of versions were restricted to the current user
::The filtering options on versions were restricted to the current user. Sometimes a user might want to view versions associated with other users. For instance, an instructor might want to view the list of versions created by a particular student. This is not possible with the current implementation.


=====Drawbacks and Solutions=====
    html += '<td><textarea cols="50" rows="1" name="question[' + self.id.to_s + '][txt]"'
* '''Problem 1''': The method paginate_list is doing more than one thing.
    html += ' id="question_' + self.id.to_s + '_txt" placeholder="Edit question content here">' + self.txt + '</textarea></td>'
::The method paginate_list was building a complex search criteria based on the input params, getting the list of versions from the Database matching this search criteria and then calling the Page API. All these tasks in a single method made it difficult to understand.
 
<pre>
     html += '<td><input size="10" disabled="disabled" value="' + self.type + '" name="question[' + self.id.to_s + '][type]"'
# For filtering the versions list with proper search and pagination.
     html += ' id="question_' + self.id.to_s + '_type" type="text"></td>'
  def paginate_list(id, user_id, item_type, event, datetime)
     # Set up the search criteria
    criteria = ''
    criteria = criteria + "id = #{id} AND " if id && id.to_i > 0
    if current_user_role? == 'Super-Administrator'
      criteria = criteria + "whodunnit = #{user_id} AND " if user_id && user_id.to_i > 0
    end
    criteria = criteria + "whodunnit = #{current_user.try(:id)} AND " if current_user.try(:id) && current_user.try(:id).to_i > 0
    criteria = criteria + "item_type = '#{item_type}' AND " if item_type && !(item_type.eql? 'Any')
     criteria = criteria + "event = '#{event}' AND " if event && !(event.eql? 'Any')
    criteria = criteria + "created_at >= '#{time_to_string(params[:start_time])}' AND "
    criteria = criteria + "created_at <= '#{time_to_string(params[:end_time])}' AND "


     if current_role == 'Instructor' || current_role == 'Administrator'
     html += '<td><input size="2" value="' + self.weight.to_s
    html += '" name="question[' + self.id.to_s + '][weight]" id="question_' + self.id.to_s + '_weight" type="text"></td>'


     end
     html += '<td>text area size <input size="3" value="' + self.size.to_s
    html += '" name="question[' + self.id.to_s + '][size]" id="question_' + self.id.to_s + '_size" type="text"></td>'


     # Remove the last ' AND '
     html += '<td> max_label <input size="10" value="' + self.max_label.to_s + '" name="question[' + self.id.to_s
     criteria = criteria[0..-5]
    html += '][max_label]" id="question_' + self.id.to_s + '_max_label" type="text">  min_label <input size="12" value="' + self.min_label.to_s
     html += '" name="question[' + self.id.to_s + '][min_label]" id="question_' + self.id.to_s + '_min_label" type="text"></td>'


     versions = Version.page(params[:page]).order('id').per_page(25).where(criteria)
     safe_join(["<tr>".html_safe, "</tr>".html_safe], html.html_safe)
    versions
   end
   end
</pre>
</pre>
* '''Solution''': The implementation has been changed in such a way that the versions which a user is allowed to see depends on the privileges of the user. The approach we have taken is as follows:
**An administrator can see all the versions
**An instructor can see all the versions created by him and other users who are in his course or are participants in the assignments he creates.
**A TA can see all the versions created by him and other users who are in the course for which he/ she assists.
**A Student can see all the versions created by him/ her.
* '''Problem 2''': The search criteria created in the method paginate_list was difficult to comprehend.
::The code which builds the search criteria in the method paginate_list uses many string literals and conditions and is hardly intuitive. The programmer will have to spend some time to understand what the code is really doing.
* '''Solution''': The implementation has been changed. A student is not allowed to delete any versions now. Other types of users, for instance administrators, instructors and TAs are allowed to delete only the versions they are authorized to view.
* '''Problem 3''': The paginate method can be moved to a helper class.
::VersionsController is not the only component which require to paginate items. There are other components too. For instance, the UsersController has to paginate the list of users. Hence the Paginate method can be moved to a helper class which can be accessed by other components as well.
* '''Solution''': The filtering options has also been enhanced. The current user can now choose as part of the version search filter any user from a list of users if the current user is authorized to see the versions created by that user.


===New Implementation===
The safe_join is later used to return the HTML string to the calling view.
*The method paginate_list has been split into 2 methods now.
 
** BuildSearchCriteria – as the name suggests the sole purpose of this method is to build a search criteria based on the input search filters when the current user initiates a search in versions.
===The view_question_text method===
** paginate_list – this method will call the paginate API.
This method has the same explanation as the edit method except the fact that this method is called only during viewing of questionnaires by an instructor. We observed that this method is also called by in student_quiz view. On further analysis, we found that the student_quiz does not use criterion object and has only three options: TrueFalse, MultipleChoiceRadio and MultipleChoiceCheckbox. So no refactoring is required here. Verified this method isn't called anywhere else using RubyMine and grep command. The outcome of this method's refactoring will completely remove this method from the model file criterion.rb as there is no business logic in this method and is completely html code.
:First the search criteria is built, then the criteria is applied to versions in the database to get all versions which matches the criteria and then the retrieved versions are paginated.
 
<pre>
<pre>
   # pagination.
   def view_question_text
  def paginate_list(versions)
    html = '<TD align="left"> ' + self.txt + ' </TD>'
    paginate(versions, VERSIONS_PER_PAGE);
    html += '<TD align="left">' + self.type + '</TD>'
  end
    html += '<td align="center">' + self.weight.to_s + '</TD>'
    questionnaire = self.questionnaire
    if !self.max_label.nil? && !self.min_label.nil?
      html += '<TD align="center"> (' + self.min_label + ') ' + questionnaire.min_question_score.to_s
      html += ' to ' + questionnaire.max_question_score.to_s + ' (' + self.max_label + ')</TD>'
    else
      html += '<TD align="center">' + questionnaire.min_question_score.to_s + ' to ' + questionnaire.max_question_score.to_s + '</TD>'
    end


  def BuildSearchCriteria(id, user_id, item_type, event)
     safe_join(["<TR>".html_safe, "</TR>".html_safe], html.html_safe)
    # Set up the search criteria
    search_criteria = ''
     search_criteria = search_criteria + add_id_filter_if_valid(id).to_s
    if current_user_role? == 'Super-Administrator'
      search_criteria = search_criteria + add_user_filter_for_super_admin(user_id).to_s
    end
    search_criteria = search_criteria + add_user_filter
    search_criteria = search_criteria + add_version_type_filter(item_type).to_s
    search_criteria = search_criteria + add_event_filter(event).to_s
    search_criteria = search_criteria + add_date_time_filter
    search_criteria
   end
   end
</pre>
</pre>
* The string literals and conditions in the method paginate_list were replaced with methods with intuitive names so that the programmer can understand the code more easily. We also removed an empty if clause and a redundant statement.
 
===The complete method===
 
This method is one of the several other methods in other files like criterion.rb which gets called when the user responds to the question being created in the respective model. In the case of this problem, which deals with criterion type question responses, the complete method defined in criterion.rb is called when the question.type is equal to "Criterion". This method allows for a user to respond to the criterion question and enter the necessary details. This method is called only from one view file i.e. response.html.erb. This file is in response view. Verified this using both RubyMine and the grep command. Unlike edit or view_question_text the self object does not refer to a global object instead it refers to a copy of the local object. So, we had passed the required object as locals to the partial, along with the parameters required by the method namely question, count, answer, questionnaire_min, questionnaire_max, dropdown_or_scale. When we move this code to a partial, we need to change this in the erb file. The outcome of this method's refactoring will completely remove this method from the model file criterion.rb as there is no business logic involved here.
 
<pre>
<pre>
  def add_id_filter_if_valid (id)
def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale)
     "id = #{id} AND " if id && id.to_i > 0
    if self.size.nil?
  end
      cols = '70'
      rows = '1'
    else
      cols = self.size.split(',')[0]
      rows = self.size.split(',')[1]
    end
 
    html = '<div><label for="responses_' + count.to_s + '">' + self.txt + '</label></div>'
    # show advice for each criterion question
    question_advices = QuestionAdvice.where(question_id: self.id).sort_by(&:id)
     advice_total_length = 0
 
    question_advices.each do |question_advice|
      advice_total_length += question_advice.advice.length if question_advice.advice && question_advice.advice != ""
    end
 
    if !question_advices.empty? and advice_total_length > 0
      html += '<a id="showAdivce_' + self.id.to_s + '" onclick="showAdvice(' + self.id.to_s + ')">Show advice</a>'
      html += '<script>'
      html += 'function showAdvice(i){'
      html += 'var element = document.getElementById("showAdivce_" + i.toString());'
      html += 'var show = element.innerHTML == "Hide advice";'
      html += 'if (show){'
      html += 'element.innerHTML="Show advice";'
      html += '}else{'
      html += 'element.innerHTML="Hide advice";}'
      html += 'toggleAdvice(i);}'
 
      html += 'function toggleAdvice(i) {'
      html += 'var elem = document.getElementById(i.toString() + "_myDiv");'
      html += 'if (elem.style.display == "none") {'
      html += 'elem.style.display = "";'
      html += '} else {'
      html += 'elem.style.display = "none";}}'
      html += '</script>'
 
      html += '<div id="' + self.id.to_s + '_myDiv" style="display: none;">'
      # [2015-10-26] Zhewei:
      # best to order advices high to low, e.g., 5 to 1
      # each level used to be a link;
      # clicking on the link caused the dropbox to be filled in with the corresponding number
      question_advices.reverse.each_with_index do |question_advice, index|
        html += '<a id="changeScore_>' + self.id.to_s + '" onclick="changeScore(' + count.to_s + ',' + index.to_s + ')">'
        html += (self.questionnaire.max_question_score - index).to_s + ' - ' + question_advice.advice + '</a><br/>'
        html += '<script>'
        html += 'function changeScore(i, j) {'
        html += 'var elem = jQuery("#responses_" + i.toString() + "_score");'
        html += 'var opts = elem.children("option").length;'
        html += 'elem.val((' + self.questionnaire.max_question_score.to_s + ' - j).toString());}'
        html += '</script>'
      end
      html += '</div>'
    end
 
    if dropdown_or_scale == 'dropdown'
      current_value = ""
      current_value += 'data-current-rating =' + answer.answer.to_s if !answer.nil?
      html += '<div><select id="responses_' + count.to_s + '_score" name="responses[' + count.to_s + '][score]" class="review-rating" ' + current_value + '>'
      html += "<option value = ''>--</option>"
      questionnaire_min.upto(questionnaire_max).each do |j|
        html += if !answer.nil? and j == answer.answer
                  '<option value=' + j.to_s + ' selected="selected">'
                else
                  '<option value=' + j.to_s + '>'
                end
 
        html += j.to_s
        if j == questionnaire_min
          html += "-" + self.min_label if self.min_label.present?
        elsif j == questionnaire_max
          html += "-" + self.max_label if self.max_label.present?
        end
        html += "</option>"
      end
      html += "</select></div><br><br>"
      html += '<textarea' + ' id="responses_' + count.to_s + '_comments"' \
      ' name="responses[' + count.to_s + '][comment]" class="tinymce">'
      html += answer.comments unless answer.nil?
      html += '</textarea></td>'
    elsif dropdown_or_scale == 'scale'
      html += '<input id="responses_' + count.to_s + '_score" name="responses[' + count.to_s + '][score]" type="hidden"'
      html += 'value="' + answer.answer.to_s + '"' unless answer.nil?
      html += '>'


  def add_user_filter_for_super_admin (user_id)
      html += '<table>'
    "whodunnit = #{user_id} AND " if user_id && user_id.to_i > 0
      html += '<tr><td width="10%"></td>'
  end
      (questionnaire_min..questionnaire_max).each do |j|
        html += '<td width="10%"><label>' + j.to_s + '</label></td>'
      end
      html += '<td width="10%"></td></tr><tr>'


  def add_user_filter
      html += if !self.min_label.nil?
    "whodunnit = #{current_user.try(:id)} AND " if current_user.try(:id) && current_user.try(:id).to_i > 0
                '<td width="10%">' + self.min_label + '</td>'
  end
              else
                '<td width="10%"></td>'
              end
      (questionnaire_min..questionnaire_max).each do |j|
        html += '<td width="10%"><input type="radio" id="' + j.to_s + '" value="' + j.to_s + '" name="Radio_' + self.id.to_s + '"'
        html += 'checked="checked"' if (!answer.nil? and answer.answer == j) or (answer.nil? and questionnaire_min == j)
        html += '></td>'
      end
      html += '<script>jQuery("input[name=Radio_' + self.id.to_s + ']:radio").change(function() {'
      html += 'var response_score = jQuery("#responses_' + count.to_s + '_score");'
      html += 'var checked_value = jQuery("input[name=Radio_' + self.id.to_s + ']:checked").val();'
      html += 'response_score.val(checked_value);});</script>'


  def add_event_filter (event)
      html += if !self.max_label.nil?
    "event = '#{event}' AND " if event && !(event.eql? 'Any')
                '<td width="10%">' + self.max_label + '</td>'
  end
              else
                '<td width="10%"></td>'
              end


  def add_date_time_filter
      html += '<td width="10%"></td></tr></table>'
    "created_at >= '#{time_to_string(params[:start_time])}' AND " +
      html += '<textarea cols=' + cols + ' rows=' + rows + ' id="responses_' + count.to_s + '_comments"' \
         "created_at <= '#{time_to_string(params[:end_time])}'"
         ' name="responses[' + count.to_s + '][comment]" class="tinymce">'
  end
      html += answer.comments unless answer.nil?
      html += '</textarea>'


  def add_version_type_filter (version_type)
    end
     "item_type = '#{version_type}' AND " if version_type && !(version_type.eql? 'Any')
     safe_join(["".html_safe, "".html_safe], html.html_safe)
   end
   end
</pre>
</pre>
* The paginate method has been moved to the helper class Pagination_Helper. This new method can be now reused by the different components like UsersController etc. The method receives two parameters, first the list to paginate and second the number of items to be displayed in a page.
 
===The view_completed_question method===
 
No changes were made to this method as this method wasn't being called from a view. Instead, it was being called by a model response.rb which was also creating an html string. We think it's best to refactor this method along with response.rb as refactoring this method without refactoring response.rb would make the code messier. More explanation is given in the next section.
 
==Proposed Solution==
All these methods contain HTML text. What we propose to do is to pick these lines of code and move them into an appropriate partial, in the required format. The next task is to find out where in the entire application do these methods get called. The multiple overriding of the method calls in this poorly structured application makes it a challenging task. These method-calls then have to be replaced with the appropriate rendering of a partial in the views. This would make the structure more MVC oriented, and help keep it clean and understandable for the next developer who accesses this file.
 
 
==Implementation==
The changes proposed above were implemented as described below.
 
===The Edit Method===
All the code in the model was just html and thus was completely removed from there. It was moved to the partial file (views/questionnaires/_criterion_edit.html.rb) and the necessary changes were made. Since it was all HTML code, no comments were actually required. However, we still added comments describing the partial file itself. Other comments were added whenever changes were made.
====Affected View Files====
'''1. views/questionnaires/edit.html.erb -''' This file calls the edit method for all types of question objects and the object type takes care of invoking the right overridden method. We modified this file to call the partial instead of edit method only for Criterion type of question objects. This view is invoked during edit on the questionnaire. Here are the changes:


<pre>
<pre>
module PaginationHelper
<% for @question in @questionnaire.questions %>
-  <%-# The following line call certain method of the object, which returns html string-%>
+  <%# E1911: Call the criterion_edit partial if question is of type Criterion -%>
+  <% if @question.is_a? Criterion %>
+    <%= render :partial => 'criterion_edit' %>
+  <% else %>
      <%=@question.edit%>
+ <% end %>
...
</pre>
 
'''2. views/questionnaires/_questionnaire.html.erb -''' This partial file is called by new.html.erb when anew question is created. Same logic and changes as above.
 
'''3. views/questionnaires/_criterion_edit.html.erb -''' This file has all the html code that was earlier in the edit method of the criterion model. We changed the variables from "self.xxx" to "@question.xxx" because self was referring to question object in this context. Apart from this we changed the code to erb format. No more changes were deemed necessary. Please refer to the pull request to refer to the changes in this file as they are pure html and really long to be put in this wiki.
 
===The view_question_text Method===
This method did not contain any business logic and thus all code was formatted and moved to a new partial file (views/questionnaires/_criterion_view.html.erb). Again, no comments were required due to complete html code and a comment describing the partial was added in addition to other necessary comments in other files.


  def paginate (items, number_of_items_per_page)
====Affected View Files====
    items.page(params[:page]).per_page(number_of_items_per_page)
'''1. views/questionnaires/view.html.erb -''' Modified to invoke the partial if question is of type Criterion only. Also changed the question variable to a instance variable (question => @question) to extend it's scope to the partial. This view is invoked during the view of questions in a questionnaire. Here are the changes:
  end


end
<pre>
- <%questions.each do |question| %>
-  <%if question.is_a? Question%>
-    <%=question.view_question_text.html_safe%>
+ <% for @question in questions %>
+  <%# E1911: Call the criterion_view partial if question is of type Criterion %>
+  <%if @question.is_a? Criterion %>
+        <%= render :partial => 'criterion_view' %>
+    <%elsif @question.is_a? Question%>
+        <%= @question.view_question_text.html_safe %>
    <%end%>
</pre>
</pre>


===Code improvements===
'''2. views/questionnaires/_criterion_view.html.erb -''' This partial file has the moved html code from view_question_text. Note that the "self.xxx" have been updated to "@question.xxx" as self referred to question object discussed in 1. Comment added to describe the partial file. Refer the pull request to look at the code.
* Introduced a constant VERSIONS_PER_PAGE and assigned the value 25 to it. The pagination algorithm for VersionsController displays at most 25 versions in a page. The existing implementation uses the value 25 straight in the code and there are few problems associated with such an approach.
 
** It is not easy to understand what 25 is unless the programmer takes a close look at the code.
===The complete Method===
** In case if the value 25 is used at more than one places and in future a new requirement comes to show at most 30 versions in a page, all the values will have to be modified. It is not very DRY.
All the code in the model included html thus was completely removed from there. It was moved to the partial file (app/views/response/_criterion_complete.html.erb) and the necessary changes were made. Since it was all HTML code, no comments were actually required. However, we still added comments describing the partial file itself. Other comments were added whenever changes were made.
* The VersionsController was overriding AccessHelper - action_allowed? method to return true in all the cases. This was violating the whole purpose of the method action_allowed?. The purpose of this method is to determine whether the user who is triggering a CRUD operation is allowed to do so. So when the current user invokes a CRUD operation, the action_allowed? method is invoked first and if the method returns true the CRUD operation is triggered or else the user is intimated with a message and gracefully exited. Hence, when the action_allowed? method is overridden to return true always, it results in providing unauthorized access to certain users.
====Affected View Files====
'''1. views/repsonse/response.html.erb -''' This file calls the complete method for all types of question objects and the object type takes care of invoking the right overridden method. We modified this file to call the partial instead of the complete method only for Criterion type of question objects. This view is invoked during user responds to the questionnaire. Here are the changes:


<pre>
<pre>
def action_allowed?
<% elsif question.instance_of? Scale %>
    true
<!--E1911: Replaced the call to criterion.complete method with the newly refactored partial-->
  end
<%= render partial: 'criterion_complete', :locals => {:question => question, :count => i, :answer => answer, :questionnaire_min => @questionnaire.min_question_score, :questionnaire_max => @questionnaire.max_question_score,
:dropdown_or_scale => @dropdown_or_scale} %>
 
<% elsif question.instance_of? Scale %>
...
</pre>
</pre>


:With the new implementation the AccessHelper - action_allowed? method has been modified in such a way that unauthorized access is prevented. As per the new algorithm, 'new', 'create', 'edit', 'update' cannot be invoked by any user. These operations can be accessed only by ‘papertrail’ gem. Only an ‘Administrator’ or ‘Super-Administrator’ can call 'destroy_all' method. All the other methods are accessible to ‘Administrator’, ‘Super-Administrator’, ‘Instructor’, ‘Teaching Assistant’ and ‘Student’.
'''2. views/response/_criterion_complete.html.erb -''' This file has all the html code that was earlier in the complete method of the criterion model. We changed the variables from "self.xxx" to "question.xxx" because self was referring to copy of question object in this context. Apart from this we changed the code to erb format. No more changes were deemed necessary. Please refer to the pull request to refer to the changes in this file as they are pure html and really long to be put in this wiki.
 
===The view_completed_question Method===
This method returns a constructed html string that is used to display a particular criterion question. Although this is similar to the other 3 methods, the html variable returned from this function is not directly called into a view. If that was the case, this could be simply moved into a partial as well.
 
Instead, this html string is appended to another larger string variable named code in the response model, which builds the entire webpage by iterative appending. The html code for a criterion question being completed is a mere "part" of this entire string, which also includes other things such as the navbar of the page, links to the buttons, other questions, the header and the footer and such.  


<pre>
<pre>
  def action_allowed?
def add_table_rows questionnaire_max, questions, answers, code, tag_prompt_deployments = nil, current_user = nil
     case params[:action]
     count = 0
     when 'new', 'create', 'edit', 'update'
     # loop through questions so the the questions are displayed in order based on seq (sequence number)
     #Modifications can only be done by papertrail
     questions.each do |question|
       return false
      count += 1 if !question.is_a? QuestionnaireHeader and question.break_before == true
    when 'destroy_all'
      answer = answers.find {|a| a.question_id == question.id }
       ['Super-Administrator',
       row_class = count.even? ? "info" : "warning"
      'Administrator'].include? current_role_name
      row_class = "" if question.is_a? QuestionnaireHeader
    else
       code += '<tr class="' + row_class + '"><td>'
      #Allow all others
      if !answer.nil? or question.is_a? QuestionnaireHeader
      ['Super-Administrator',
        code += if question.instance_of? Criterion
      'Administrator',
                  #Answer Tags are enabled only for Criterion questions at the moment.
      'Instructor',
                  question.view_completed_question(count, answer, questionnaire_max, tag_prompt_deployments, current_user) || ''
      'Teaching Assistant',
                elsif question.instance_of? Scale
      'Student'].include? current_role_name
                  question.view_completed_question(count, answer, questionnaire_max) || ''
                else
                  question.view_completed_question(count, answer) || ''
                end
      end
      code += '</td></tr>'
     end
     end
    code
   end
   end
</pre>
</pre>


===Automated Testing using RSPEC===
This "code" variable is then used to display the entire page through the view method under response.rb.
The current version of expertiza did not have any test for VersionsController. Using the test driven development(TDD) approach, we have added an exhaustive set of RSPEC tests for VersionsController, to test all the modifications we have done to the code of the controller class. The tests use double and stub features of rspec-rails gem, to fake the log in by different users - Administrator, Instructor, Student etc. The tests can be executed "rpec spec" command as shown below.
 
<pre>
A partial cannot be rendered halfway through the computation of "code", neither can it be called from a model. Since code is containing the html string, and not erb, we cannot append the render partial line to the code variable as well.
user-expertiza $rspec spec
 
.
To refactor this method, we need to first refactor the entire response.rb to not append the html string for an entire webpage in a single variable, but rather use appropriate views to render the webpage.
.
.
Finished in 5.39 seconds (files took 25.33 seconds to load)
66 examples, 0 failures


Randomized with seed 19254
Refactoring response model would require changes to other question subclasses like scale along with changes to the model itself. We think that it is best to refactor this method when response model is refactored along with other affected subclasses. Also, refactoring an entire model would be beyond the scope of this project as the scale of the task is that of an entire project by itself.
.
.
</pre>


===Testing from UI===
Thus, this method has been left untouched and still remains in criterion.rb itself.
Following are a few testcases with respectto our code changes that can be tried from UI:
1. To go to versions index page, type in the following url after logging in:
  http://152.46.16.81:3000/versions


2. After logging in as student/instructor or admin : Try accessing the  new, create, edit, update actions. These actions are not allowed to any of the users.
==Testing==
  http://152.46.16.81:3000/versions/new
  This calls the new action. In the current production version of expertiza, it is unhandled and application gives a default 404 page.


3. Another feature that can be tested from UI is Pagination. Try searching for a user's versions and see if the results are paginated or not. Search here:
Since our project only dealt with refactoring existing code, there was no particular need to test any specific components of the application. That being said, there were previous tests that were testing if the html variable returned by those methods was in the expected format or not.
  http://152.46.16.81:3000/versions/search
<pre>
  describe "#edit" do
    it "returns the html " do
      html = criterion.edit(0).to_s
      expect(html).to eq("<tr><td align=\"center\"><a rel=\"nofollow\" data-method=\"delete\" href=\"/questions/1\">Remove</a></td><td>
<input size=\"6\" value=\"1.0\" name=\"question[1][seq]\" id=\"question_1_seq\" type=\"text\"></td><td><textarea cols=\"50\" rows=\"1\"
name=\"question[1][txt]\" id=\"question_1_txt\" placeholder=\"Edit question content here\">test txt</textarea></td><td><input size=\"10\"
disabled=\"disabled\" value=\"Criterion\" name=\"question[1][type]\" id=\"question_1_type\" type=\"text\"></td><td><input size=\"2\"
value=\"1\" name=\"question[1][weight]\" id=\"question_1_weight\" type=\"text\"></td><td>text area size <input size=\"3\" value=\"\"
name=\"question[1][size]\" id=\"question_1_size\" type=\"text\"></td><td> max_label <input size=\"10\" value=\"\" name=\"question[1]
[max_label]\" id=\"question_1_max_label\" type=\"text\">  min_label <input size=\"12\" value=\"\" name=\"question[1][min_label]\"
id=\"question_1_min_label\" type=\"text\"></td></tr>")
    end
  end
</pre>


4. Visit the same URL as step 3, you should see only the students under that instructor in the users dropdown.
<pre>
  describe "#view_question_text" do
    it "returns the html " do
      html = criterion.view_question_text.to_s
      expect(html).to eq("<TR><TD align=\"left\"> test txt </TD><TD align=\"left\">Criterion</TD><td align=\"center\">1</TD><TD align=\"center\"> () 0 to 5 ()</TD></TR>")
    end
  end
</pre>


===References===
<pre>
  describe "#complete" do
    it "returns the html " do
      html = criterion.complete(0, nil, 0, 5).to_s
      expect(html).to eq("<div><label for=\"responses_0\">test txt</label></div>")
    end
  end
</pre>


#[https://github.com/expertiza/expertiza Expertiza on GitHub]
Now that these methods do not exist, there is no need to keep these test files as there is no html variable being returned or parsed. Therefore, we have removed these redundant test cases.
#[https://github.com/WintersLt/expertiza GitHub Project Repository Fork]
#[http://expertiza.ncsu.edu/ The live Expertiza website]
#[http://bit.ly/myexpertiza  Demo link]
#[http://wikis.lib.ncsu.edu/index.php/Expertiza Expertiza project documentation wiki]
#[https://relishapp.com/rspec Rspec Documentation]
#Clean Code: A handbook of agile software craftsmanship. Author: Robert.C.Martin

Latest revision as of 04:00, 2 April 2019

E1911 Refactoring criterion.rb

This page provides a description of the Expertiza based OSS project.



About Expertiza

Expertiza is an open source project based on Ruby on Rails framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.

Problem Statement

The bulk of the code in criterion is HTML. It is the violation of MVC architecture - Model should not concern itself with how the data is displayed. This code needs to be moved to a partial file, and the partial file needs to be called in all appropriate places which call the criterion’s model methods. Once the logic for the view is moved out of the model, the model should only be left with business logic. This business logic code can also be refactored. Plus there are virtually no comments, properly comment the code.

About Criterion.rb

Criterion is one of the types of questions that can be added to a questionnaire. There are many other types of question objects like dropdown that have the implementation of similar methods. As of now, the criterion model holds 4 methods that are called from different places in the application. Each of these methods is storing a string value which contains the necessary HTML lines to display the required functionalities to the user. This string value is then returned to the calling methods from the respective views. This HTML string is then entered wherever necessary. This is ideally the exact function of a partial, and this page needs to be heavily reformatted to use partials instead of returning a string containing html code.

The pull request

https://github.com/expertiza/expertiza/pull/1407

The edit method

This method is one of the several other methods in other files like criterion.rb which gets called when the type of the question being created in the respective model. In the case of this problem, which deals with criterion type questions, the edit method defined in criterion.rb is called when the question.type is equal to "Criterion". This method allows for a user to create the criterion question and enter the necessary details. This method is called only from two view files - one for creating (_questionnaire.html.erb partial file) and another for editing (edit.html.erb). Both these files are in questionnaires view. Verified this using both RubyMine and the grep command. Another interesting thing to note is the use of "self." to get the attributes associated with question object. When we move this code to a partial, we need to change this in the erb file. The outcome of this method's refactoring will completely remove this method from the model file criterion.rb as there is no business logic involved here.

def edit(_count)
    html = '<td align="center"><a rel="nofollow" data-method="delete" href="/questions/' + self.id.to_s + '">Remove</a></td>'

    html += '<td><input size="6" value="' + self.seq.to_s + '" name="question[' + self.id.to_s + '][seq]"'
    html += ' id="question_' + self.id.to_s + '_seq" type="text"></td>'

    html += '<td><textarea cols="50" rows="1" name="question[' + self.id.to_s + '][txt]"'
    html += ' id="question_' + self.id.to_s + '_txt" placeholder="Edit question content here">' + self.txt + '</textarea></td>'

    html += '<td><input size="10" disabled="disabled" value="' + self.type + '" name="question[' + self.id.to_s + '][type]"'
    html += ' id="question_' + self.id.to_s + '_type" type="text"></td>'

    html += '<td><input size="2" value="' + self.weight.to_s
    html += '" name="question[' + self.id.to_s + '][weight]" id="question_' + self.id.to_s + '_weight" type="text"></td>'

    html += '<td>text area size <input size="3" value="' + self.size.to_s
    html += '" name="question[' + self.id.to_s + '][size]" id="question_' + self.id.to_s + '_size" type="text"></td>'

    html += '<td> max_label <input size="10" value="' + self.max_label.to_s + '" name="question[' + self.id.to_s
    html += '][max_label]" id="question_' + self.id.to_s + '_max_label" type="text">  min_label <input size="12" value="' + self.min_label.to_s
    html += '" name="question[' + self.id.to_s + '][min_label]" id="question_' + self.id.to_s + '_min_label" type="text"></td>'

    safe_join(["<tr>".html_safe, "</tr>".html_safe], html.html_safe)
  end

The safe_join is later used to return the HTML string to the calling view.

The view_question_text method

This method has the same explanation as the edit method except the fact that this method is called only during viewing of questionnaires by an instructor. We observed that this method is also called by in student_quiz view. On further analysis, we found that the student_quiz does not use criterion object and has only three options: TrueFalse, MultipleChoiceRadio and MultipleChoiceCheckbox. So no refactoring is required here. Verified this method isn't called anywhere else using RubyMine and grep command. The outcome of this method's refactoring will completely remove this method from the model file criterion.rb as there is no business logic in this method and is completely html code.

  def view_question_text
    html = '<TD align="left"> ' + self.txt + ' </TD>'
    html += '<TD align="left">' + self.type + '</TD>'
    html += '<td align="center">' + self.weight.to_s + '</TD>'
    questionnaire = self.questionnaire
    if !self.max_label.nil? && !self.min_label.nil?
      html += '<TD align="center"> (' + self.min_label + ') ' + questionnaire.min_question_score.to_s
      html += ' to ' + questionnaire.max_question_score.to_s + ' (' + self.max_label + ')</TD>'
    else
      html += '<TD align="center">' + questionnaire.min_question_score.to_s + ' to ' + questionnaire.max_question_score.to_s + '</TD>'
    end

    safe_join(["<TR>".html_safe, "</TR>".html_safe], html.html_safe)
  end

The complete method

This method is one of the several other methods in other files like criterion.rb which gets called when the user responds to the question being created in the respective model. In the case of this problem, which deals with criterion type question responses, the complete method defined in criterion.rb is called when the question.type is equal to "Criterion". This method allows for a user to respond to the criterion question and enter the necessary details. This method is called only from one view file i.e. response.html.erb. This file is in response view. Verified this using both RubyMine and the grep command. Unlike edit or view_question_text the self object does not refer to a global object instead it refers to a copy of the local object. So, we had passed the required object as locals to the partial, along with the parameters required by the method namely question, count, answer, questionnaire_min, questionnaire_max, dropdown_or_scale. When we move this code to a partial, we need to change this in the erb file. The outcome of this method's refactoring will completely remove this method from the model file criterion.rb as there is no business logic involved here.

def complete(count, answer = nil, questionnaire_min, questionnaire_max, dropdown_or_scale)
    if self.size.nil?
      cols = '70'
      rows = '1'
    else
      cols = self.size.split(',')[0]
      rows = self.size.split(',')[1]
    end

    html = '<div><label for="responses_' + count.to_s + '">' + self.txt + '</label></div>'
    # show advice for each criterion question
    question_advices = QuestionAdvice.where(question_id: self.id).sort_by(&:id)
    advice_total_length = 0

    question_advices.each do |question_advice|
      advice_total_length += question_advice.advice.length if question_advice.advice && question_advice.advice != ""
    end

    if !question_advices.empty? and advice_total_length > 0
      html += '<a id="showAdivce_' + self.id.to_s + '" onclick="showAdvice(' + self.id.to_s + ')">Show advice</a>'
      html += '<script>'
      html += 'function showAdvice(i){'
      html += 'var element = document.getElementById("showAdivce_" + i.toString());'
      html += 'var show = element.innerHTML == "Hide advice";'
      html += 'if (show){'
      html += 'element.innerHTML="Show advice";'
      html += '}else{'
      html += 'element.innerHTML="Hide advice";}'
      html += 'toggleAdvice(i);}'

      html += 'function toggleAdvice(i) {'
      html += 'var elem = document.getElementById(i.toString() + "_myDiv");'
      html += 'if (elem.style.display == "none") {'
      html += 'elem.style.display = "";'
      html += '} else {'
      html += 'elem.style.display = "none";}}'
      html += '</script>'

      html += '<div id="' + self.id.to_s + '_myDiv" style="display: none;">'
      # [2015-10-26] Zhewei:
      # best to order advices high to low, e.g., 5 to 1
      # each level used to be a link;
      # clicking on the link caused the dropbox to be filled in with the corresponding number
      question_advices.reverse.each_with_index do |question_advice, index|
        html += '<a id="changeScore_>' + self.id.to_s + '" onclick="changeScore(' + count.to_s + ',' + index.to_s + ')">'
        html += (self.questionnaire.max_question_score - index).to_s + ' - ' + question_advice.advice + '</a><br/>'
        html += '<script>'
        html += 'function changeScore(i, j) {'
        html += 'var elem = jQuery("#responses_" + i.toString() + "_score");'
        html += 'var opts = elem.children("option").length;'
        html += 'elem.val((' + self.questionnaire.max_question_score.to_s + ' - j).toString());}'
        html += '</script>'
      end
      html += '</div>'
    end

    if dropdown_or_scale == 'dropdown'
      current_value = ""
      current_value += 'data-current-rating =' + answer.answer.to_s if !answer.nil?
      html += '<div><select id="responses_' + count.to_s + '_score" name="responses[' + count.to_s + '][score]" class="review-rating" ' + current_value + '>'
      html += "<option value = ''>--</option>"
      questionnaire_min.upto(questionnaire_max).each do |j|
        html += if !answer.nil? and j == answer.answer
                  '<option value=' + j.to_s + ' selected="selected">'
                else
                  '<option value=' + j.to_s + '>'
                end

        html += j.to_s
        if j == questionnaire_min
          html += "-" + self.min_label if self.min_label.present?
        elsif j == questionnaire_max
          html += "-" + self.max_label if self.max_label.present?
        end
        html += "</option>"
      end
      html += "</select></div><br><br>"
      html += '<textarea' + ' id="responses_' + count.to_s + '_comments"' \
       ' name="responses[' + count.to_s + '][comment]" class="tinymce">'
      html += answer.comments unless answer.nil?
      html += '</textarea></td>'
    elsif dropdown_or_scale == 'scale'
      html += '<input id="responses_' + count.to_s + '_score" name="responses[' + count.to_s + '][score]" type="hidden"'
      html += 'value="' + answer.answer.to_s + '"' unless answer.nil?
      html += '>'

      html += '<table>'
      html += '<tr><td width="10%"></td>'
      (questionnaire_min..questionnaire_max).each do |j|
        html += '<td width="10%"><label>' + j.to_s + '</label></td>'
      end
      html += '<td width="10%"></td></tr><tr>'

      html += if !self.min_label.nil?
                '<td width="10%">' + self.min_label + '</td>'
              else
                '<td width="10%"></td>'
              end
      (questionnaire_min..questionnaire_max).each do |j|
        html += '<td width="10%"><input type="radio" id="' + j.to_s + '" value="' + j.to_s + '" name="Radio_' + self.id.to_s + '"'
        html += 'checked="checked"' if (!answer.nil? and answer.answer == j) or (answer.nil? and questionnaire_min == j)
        html += '></td>'
      end
      html += '<script>jQuery("input[name=Radio_' + self.id.to_s + ']:radio").change(function() {'
      html += 'var response_score = jQuery("#responses_' + count.to_s + '_score");'
      html += 'var checked_value = jQuery("input[name=Radio_' + self.id.to_s + ']:checked").val();'
      html += 'response_score.val(checked_value);});</script>'

      html += if !self.max_label.nil?
                '<td width="10%">' + self.max_label + '</td>'
              else
                '<td width="10%"></td>'
              end

      html += '<td width="10%"></td></tr></table>'
      html += '<textarea cols=' + cols + ' rows=' + rows + ' id="responses_' + count.to_s + '_comments"' \
        ' name="responses[' + count.to_s + '][comment]" class="tinymce">'
      html += answer.comments unless answer.nil?
      html += '</textarea>'

    end
    safe_join(["".html_safe, "".html_safe], html.html_safe)
  end

The view_completed_question method

No changes were made to this method as this method wasn't being called from a view. Instead, it was being called by a model response.rb which was also creating an html string. We think it's best to refactor this method along with response.rb as refactoring this method without refactoring response.rb would make the code messier. More explanation is given in the next section.

Proposed Solution

All these methods contain HTML text. What we propose to do is to pick these lines of code and move them into an appropriate partial, in the required format. The next task is to find out where in the entire application do these methods get called. The multiple overriding of the method calls in this poorly structured application makes it a challenging task. These method-calls then have to be replaced with the appropriate rendering of a partial in the views. This would make the structure more MVC oriented, and help keep it clean and understandable for the next developer who accesses this file.


Implementation

The changes proposed above were implemented as described below.

The Edit Method

All the code in the model was just html and thus was completely removed from there. It was moved to the partial file (views/questionnaires/_criterion_edit.html.rb) and the necessary changes were made. Since it was all HTML code, no comments were actually required. However, we still added comments describing the partial file itself. Other comments were added whenever changes were made.

Affected View Files

1. views/questionnaires/edit.html.erb - This file calls the edit method for all types of question objects and the object type takes care of invoking the right overridden method. We modified this file to call the partial instead of edit method only for Criterion type of question objects. This view is invoked during edit on the questionnaire. Here are the changes:

<% for @question in @questionnaire.questions %>
-   <%-# The following line call certain method of the object, which returns html string-%>
+   <%# E1911: Call the criterion_edit partial if question is of type Criterion -%>
+   <% if @question.is_a? Criterion %>
+     <%= render :partial => 'criterion_edit' %>
+   <% else %>
      <%=@question.edit%>
+ <% end %>
...

2. views/questionnaires/_questionnaire.html.erb - This partial file is called by new.html.erb when anew question is created. Same logic and changes as above.

3. views/questionnaires/_criterion_edit.html.erb - This file has all the html code that was earlier in the edit method of the criterion model. We changed the variables from "self.xxx" to "@question.xxx" because self was referring to question object in this context. Apart from this we changed the code to erb format. No more changes were deemed necessary. Please refer to the pull request to refer to the changes in this file as they are pure html and really long to be put in this wiki.

The view_question_text Method

This method did not contain any business logic and thus all code was formatted and moved to a new partial file (views/questionnaires/_criterion_view.html.erb). Again, no comments were required due to complete html code and a comment describing the partial was added in addition to other necessary comments in other files.

Affected View Files

1. views/questionnaires/view.html.erb - Modified to invoke the partial if question is of type Criterion only. Also changed the question variable to a instance variable (question => @question) to extend it's scope to the partial. This view is invoked during the view of questions in a questionnaire. Here are the changes:

- <%questions.each do |question| %>
-   <%if question.is_a? Question%>
-     <%=question.view_question_text.html_safe%>
+ <% for @question in questions %>
+   <%# E1911: Call the criterion_view partial if question is of type Criterion %>
+   <%if @question.is_a? Criterion %>
+         <%= render :partial => 'criterion_view' %>
+     <%elsif @question.is_a? Question%>
+         <%= @question.view_question_text.html_safe %>
    <%end%>

2. views/questionnaires/_criterion_view.html.erb - This partial file has the moved html code from view_question_text. Note that the "self.xxx" have been updated to "@question.xxx" as self referred to question object discussed in 1. Comment added to describe the partial file. Refer the pull request to look at the code.

The complete Method

All the code in the model included html thus was completely removed from there. It was moved to the partial file (app/views/response/_criterion_complete.html.erb) and the necessary changes were made. Since it was all HTML code, no comments were actually required. However, we still added comments describing the partial file itself. Other comments were added whenever changes were made.

Affected View Files

1. views/repsonse/response.html.erb - This file calls the complete method for all types of question objects and the object type takes care of invoking the right overridden method. We modified this file to call the partial instead of the complete method only for Criterion type of question objects. This view is invoked during user responds to the questionnaire. Here are the changes:

<% elsif question.instance_of? Scale %>
<!--E1911: Replaced the call to criterion.complete method with the newly refactored partial-->
<%= render partial: 'criterion_complete', :locals => {:question => question, :count => i, :answer => answer, :questionnaire_min => @questionnaire.min_question_score, :questionnaire_max => @questionnaire.max_question_score, 
:dropdown_or_scale => @dropdown_or_scale} %>

<% elsif question.instance_of? Scale %>
...

2. views/response/_criterion_complete.html.erb - This file has all the html code that was earlier in the complete method of the criterion model. We changed the variables from "self.xxx" to "question.xxx" because self was referring to copy of question object in this context. Apart from this we changed the code to erb format. No more changes were deemed necessary. Please refer to the pull request to refer to the changes in this file as they are pure html and really long to be put in this wiki.

The view_completed_question Method

This method returns a constructed html string that is used to display a particular criterion question. Although this is similar to the other 3 methods, the html variable returned from this function is not directly called into a view. If that was the case, this could be simply moved into a partial as well.

Instead, this html string is appended to another larger string variable named code in the response model, which builds the entire webpage by iterative appending. The html code for a criterion question being completed is a mere "part" of this entire string, which also includes other things such as the navbar of the page, links to the buttons, other questions, the header and the footer and such.

def add_table_rows questionnaire_max, questions, answers, code, tag_prompt_deployments = nil, current_user = nil
    count = 0
    # loop through questions so the the questions are displayed in order based on seq (sequence number)
    questions.each do |question|
      count += 1 if !question.is_a? QuestionnaireHeader and question.break_before == true
      answer = answers.find {|a| a.question_id == question.id }
      row_class = count.even? ? "info" : "warning"
      row_class = "" if question.is_a? QuestionnaireHeader
      code += '<tr class="' + row_class + '"><td>'
      if !answer.nil? or question.is_a? QuestionnaireHeader
        code += if question.instance_of? Criterion
                  #Answer Tags are enabled only for Criterion questions at the moment.
                  question.view_completed_question(count, answer, questionnaire_max, tag_prompt_deployments, current_user) || ''
                elsif question.instance_of? Scale
                  question.view_completed_question(count, answer, questionnaire_max) || ''
                else
                  question.view_completed_question(count, answer) || ''
                end
      end
      code += '</td></tr>'
    end
    code
  end

This "code" variable is then used to display the entire page through the view method under response.rb.

A partial cannot be rendered halfway through the computation of "code", neither can it be called from a model. Since code is containing the html string, and not erb, we cannot append the render partial line to the code variable as well.

To refactor this method, we need to first refactor the entire response.rb to not append the html string for an entire webpage in a single variable, but rather use appropriate views to render the webpage.

Refactoring response model would require changes to other question subclasses like scale along with changes to the model itself. We think that it is best to refactor this method when response model is refactored along with other affected subclasses. Also, refactoring an entire model would be beyond the scope of this project as the scale of the task is that of an entire project by itself.

Thus, this method has been left untouched and still remains in criterion.rb itself.

Testing

Since our project only dealt with refactoring existing code, there was no particular need to test any specific components of the application. That being said, there were previous tests that were testing if the html variable returned by those methods was in the expected format or not.

  describe "#edit" do
    it "returns the html " do
      html = criterion.edit(0).to_s
      expect(html).to eq("<tr><td align=\"center\"><a rel=\"nofollow\" data-method=\"delete\" href=\"/questions/1\">Remove</a></td><td>
<input size=\"6\" value=\"1.0\" name=\"question[1][seq]\" id=\"question_1_seq\" type=\"text\"></td><td><textarea cols=\"50\" rows=\"1\" 
name=\"question[1][txt]\" id=\"question_1_txt\" placeholder=\"Edit question content here\">test txt</textarea></td><td><input size=\"10\"
 disabled=\"disabled\" value=\"Criterion\" name=\"question[1][type]\" id=\"question_1_type\" type=\"text\"></td><td><input size=\"2\" 
value=\"1\" name=\"question[1][weight]\" id=\"question_1_weight\" type=\"text\"></td><td>text area size <input size=\"3\" value=\"\" 
name=\"question[1][size]\" id=\"question_1_size\" type=\"text\"></td><td> max_label <input size=\"10\" value=\"\" name=\"question[1]
[max_label]\" id=\"question_1_max_label\" type=\"text\">  min_label <input size=\"12\" value=\"\" name=\"question[1][min_label]\" 
id=\"question_1_min_label\" type=\"text\"></td></tr>")
    end
  end
  describe "#view_question_text" do
    it "returns the html " do
      html = criterion.view_question_text.to_s
      expect(html).to eq("<TR><TD align=\"left\"> test txt </TD><TD align=\"left\">Criterion</TD><td align=\"center\">1</TD><TD align=\"center\"> () 0 to 5 ()</TD></TR>")
    end
  end
  describe "#complete" do
    it "returns the html " do
      html = criterion.complete(0, nil, 0, 5).to_s
      expect(html).to eq("<div><label for=\"responses_0\">test txt</label></div>")
    end
  end

Now that these methods do not exist, there is no need to keep these test files as there is no html variable being returned or parsed. Therefore, we have removed these redundant test cases.