<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Mdbrain2</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Mdbrain2"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Mdbrain2"/>
	<updated>2026-05-16T19:39:19Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150171</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150171"/>
		<updated>2023-04-25T23:16:41Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: /* Issue 6 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
This project is a continuation to E2312. The current project is to make design changes to the Response class, ReviewCommentMixin module, ScorableMixin module and add comments to test cases. We explain the issue with respect to the current implementation of each of these entities as subsections below.&lt;br /&gt;
&lt;br /&gt;
==Note to the reviwer for final submission==&lt;br /&gt;
* This project is a refactoring of some files done in Project 3. We have made these changes in a separate repo so that the new changes can be seen clearly.&lt;br /&gt;
&lt;br /&gt;
* We have added a new section in this documentation called '''Changes made / Solutions''' that has all the changes we have made and explanations. &lt;br /&gt;
&lt;br /&gt;
'''PR for this project:''' https://github.com/expertiza/reimplementation-back-end/pull/38&lt;br /&gt;
&lt;br /&gt;
'''The PR for the previous project (Project 3) on top of which these changes have been made:''' https://github.com/expertiza/reimplementation-back-end/pull/23&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Issues==&lt;br /&gt;
&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactoring significant_difference&lt;br /&gt;
* Make the method in ReviewCommentMixin to be an instance method&lt;br /&gt;
* Simplify + Refactor methods in ScorableMixin&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.  This will allow the client class to use an Iterator pattern to fetch one review at a time from the Response class rather than iterate over that data structure that get_all_review_comments returned to it.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* 1) Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
* 2) Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
* 3) Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
  assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
    map.assignment&lt;br /&gt;
  else&lt;br /&gt;
    Participant.find(map.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
* 1) The common function would involve calculating the sum of weights. &lt;br /&gt;
&lt;br /&gt;
  def calculate_total_weight()&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
  end&lt;br /&gt;
  And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 2) Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
  question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
  questions = Question.where(question_id: question_ids)&lt;br /&gt;
  &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 3) To solve checking the object type, we can write a method named '''response_assignment''' in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return its assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes made / Solutions==&lt;br /&gt;
&lt;br /&gt;
===Issue 1===&lt;br /&gt;
The get_all_review_comments method has been moved to be an instance method of the assignment class and modified to only take the reviewee_id. However, the return value has not been modified. This is because upon further investigation the get_all_review_comments (previously called concatenate_all_review_comments) is only used within the volume_of_review_comments method and that modifying volume_of_review_comments to only get a single variable from get_all_review_comments proved to difficult. Unlike the avg_scores_and_count_for_prev_reviews, however, get_all_review_comments was not merged into get_all_review_comments due to its size and that doing so would make get_all_review_comments difficult to understand.&lt;br /&gt;
&lt;br /&gt;
===Issue 2===&lt;br /&gt;
The avg_scores_and_count_for_prev_reviews method has been merged with the significant_difference? method.&lt;br /&gt;
&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    # gets all responses made by a reviewee&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    #&lt;br /&gt;
    count = 0&lt;br /&gt;
    total = 0&lt;br /&gt;
    # gets the sum total percentage scores of all responses that are not this response&lt;br /&gt;
    existing_responses.each do |response|&lt;br /&gt;
      unless id == response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        total +=  response.aggregate_questionnaire_score.to_f / response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    #&lt;br /&gt;
    # if this response is the only response by the reviewee, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    #&lt;br /&gt;
    # calculates the average score of all other responses&lt;br /&gt;
    average_score = total / count&lt;br /&gt;
    #&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    #&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    #&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3===&lt;br /&gt;
We have made the volume_of_review_comments method to be an instance method and moved it to the assignment.rb since it used to take a 'assignment_id' as an input argument. In this way, we are able to make it more sense and removed the ReviewCommentMixin.&lt;br /&gt;
&lt;br /&gt;
  # frozen_string_literal: true&lt;br /&gt;
  module MetricHelper&lt;br /&gt;
    # Determine the average size of review comments in each round&lt;br /&gt;
    # the first entry in the returned list is the overall average&lt;br /&gt;
    # word count.&lt;br /&gt;
    def volume_of_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      comments, counter,&lt;br /&gt;
        @comments_in_round, @counter_in_round = Response.get_all_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      # Index 0 is a nil element that can be ignored in the round count&lt;br /&gt;
      num_rounds = @comments_in_round.count - 1&lt;br /&gt;
  &lt;br /&gt;
      overall_avg_vol = (Lingua::EN::Readability.new(comments).num_words / (counter.zero? ? 1 : counter)).round(0)&lt;br /&gt;
      review_comments_volume = []&lt;br /&gt;
      review_comments_volume.push(overall_avg_vol)&lt;br /&gt;
      (1..num_rounds).each do |round|&lt;br /&gt;
        num = Lingua::EN::Readability.new(@comments_in_round[round]).num_words&lt;br /&gt;
        den = (@counter_in_round[round].zero? ? 1 : @counter_in_round[round])&lt;br /&gt;
        avg_vol_in_round = (num / den).round(0)&lt;br /&gt;
        review_comments_volume.push(avg_vol_in_round)&lt;br /&gt;
      end&lt;br /&gt;
      review_comments_volume&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 4===&lt;br /&gt;
* 1) We have decided not to address this part of the issue (See the Issue 4) after discussing with the professor. The reason behind it is that the lines involved in this section is only 2 lines long and hence it does not have to be moved to a method.&lt;br /&gt;
&lt;br /&gt;
* 2) We looked at a lot of solutions to make this. The issue is that using just a 'where' method will not ensure that the Active Record Association returned will be in the same order of that of the list of ids given.&lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
&lt;br /&gt;
  ids = [4,1,2,7]&lt;br /&gt;
  answers = Answer.where(id: ids)&lt;br /&gt;
  return answers.map(&amp;amp;:id)&lt;br /&gt;
&lt;br /&gt;
This example snippet would return 1,2,4,7&lt;br /&gt;
&lt;br /&gt;
Hence, to ensure the order of the return values(thereby ensuring the correct working) and to improve the performance, we have used a small gem 'find_with_order'. The main reason why we decided to add this gem over a custom solution is portability / ease of modification. A custom solution would have required us to write SQL queries in where clauses and this would be tied to the mysql(current database used). This gem is compatible with popular databases and hence this would be automatically taken care of even if the database is changed.&lt;br /&gt;
&lt;br /&gt;
Hence, our final solution is:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    def calculate_total_score&lt;br /&gt;
      # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
      # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
      sum = 0&lt;br /&gt;
      question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
      # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
      questions = Question.find_with_order(question_ids)&lt;br /&gt;
      scores.each_with_index do |score, idx|&lt;br /&gt;
        sum += score.answer * questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
      end&lt;br /&gt;
      sum&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
  def maximum_score&lt;br /&gt;
    # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
    # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
    total_weight = 0&lt;br /&gt;
    question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
    # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
    questions = Question.find_with_order(question_ids)&lt;br /&gt;
    scores.each_with_index do |score, idx|&lt;br /&gt;
      total_weight += questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
    end&lt;br /&gt;
    questionnaire = if scores.empty?&lt;br /&gt;
                      questionnaire_by_answer(nil)&lt;br /&gt;
                    else&lt;br /&gt;
                      questionnaire_by_answer(scores.first)&lt;br /&gt;
                    end&lt;br /&gt;
    total_weight * questionnaire.max_question_score&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
* 3) Usage of visitor pattern to avoid checking class types: We solved this by adding a method '''response_assignment''' to ReviewResponseMap and ResponseMap classes.&lt;br /&gt;
&lt;br /&gt;
In response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return Participant.find(self.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
In review_response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the review response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return self.assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Hence, the '''questionnaire_by_answer''' method in '''ScorableHelper''' will have the following snippet:&lt;br /&gt;
&lt;br /&gt;
      map = ResponseMap.find(map_id)&lt;br /&gt;
      assignment = map.response_assignment&lt;br /&gt;
&lt;br /&gt;
===Issue 5===&lt;br /&gt;
The get_all_responses method has been made an instance method of response_map and simplified as the function no longer need to find a response map.&lt;br /&gt;
&lt;br /&gt;
  def get_all_responses&lt;br /&gt;
    Response.where(map_id: map_id).all&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 6===&lt;br /&gt;
Additional comments have been made to the tests for the response class.&lt;br /&gt;
&lt;br /&gt;
==Design Principles==&lt;br /&gt;
This section aims to act as a consolidation of the design principles that we will implement as part of this project&lt;br /&gt;
* DRY: We will DRY the code that we handle as part of this project. An example of this would be the change that we make as part of the ScorableMixin (see Issue 4).&lt;br /&gt;
&lt;br /&gt;
* Visitor Pattern: Having to check the class of an object and making calls based on that is an example of a bad design pattern. An example of this is ScorableMixin. We will adopt a loose implementation of the Visitor pattern to overcome this by adding a method in ResponseMap and ReviewResponseMap and just call that method in the object we receive. See Issue 4 section for a detailed explanation.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
Since this project is tied to the Response class, all the test cases will be added to response_spec.rb&lt;br /&gt;
&lt;br /&gt;
* Issue 1,2,4: Modify tests in response_spec.rb&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;br /&gt;
* Test comments for review - We also want to test the scenario that involves seeing the comments for a review.&lt;br /&gt;
&lt;br /&gt;
==Future Work==&lt;br /&gt;
The issues pointed out in this documentation will be implemented once the design gets approved. Further changes will be updated in this section as we make progress with the project.&lt;br /&gt;
&lt;br /&gt;
==Mentors==&lt;br /&gt;
* Prof. Edward F. Gehringer&lt;br /&gt;
&lt;br /&gt;
==Contributors==&lt;br /&gt;
* Arvind Srinivas Subramanian (asubram9)&lt;br /&gt;
* David Brain (mdbrain2)&lt;br /&gt;
* Dhanya Sri Dasari (ddasari)&lt;br /&gt;
* Zhihao Wang (zwang238@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150170</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150170"/>
		<updated>2023-04-25T23:15:11Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: /* Issue 5 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
This project is a continuation to E2312. The current project is to make design changes to the Response class, ReviewCommentMixin module, ScorableMixin module and add comments to test cases. We explain the issue with respect to the current implementation of each of these entities as subsections below.&lt;br /&gt;
&lt;br /&gt;
==Note to the reviwer for final submission==&lt;br /&gt;
* This project is a refactoring of some files done in Project 3. We have made these changes in a separate repo so that the new changes can be seen clearly.&lt;br /&gt;
&lt;br /&gt;
* We have added a new section in this documentation called '''Changes made / Solutions''' that has all the changes we have made and explanations. &lt;br /&gt;
&lt;br /&gt;
'''PR for this project:''' https://github.com/expertiza/reimplementation-back-end/pull/38&lt;br /&gt;
&lt;br /&gt;
'''The PR for the previous project (Project 3) on top of which these changes have been made:''' https://github.com/expertiza/reimplementation-back-end/pull/23&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Issues==&lt;br /&gt;
&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactoring significant_difference&lt;br /&gt;
* Make the method in ReviewCommentMixin to be an instance method&lt;br /&gt;
* Simplify + Refactor methods in ScorableMixin&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.  This will allow the client class to use an Iterator pattern to fetch one review at a time from the Response class rather than iterate over that data structure that get_all_review_comments returned to it.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* 1) Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
* 2) Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
* 3) Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
  assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
    map.assignment&lt;br /&gt;
  else&lt;br /&gt;
    Participant.find(map.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
* 1) The common function would involve calculating the sum of weights. &lt;br /&gt;
&lt;br /&gt;
  def calculate_total_weight()&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
  end&lt;br /&gt;
  And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 2) Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
  question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
  questions = Question.where(question_id: question_ids)&lt;br /&gt;
  &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 3) To solve checking the object type, we can write a method named '''response_assignment''' in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return its assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes made / Solutions==&lt;br /&gt;
&lt;br /&gt;
===Issue 1===&lt;br /&gt;
The get_all_review_comments method has been moved to be an instance method of the assignment class and modified to only take the reviewee_id. However, the return value has not been modified. This is because upon further investigation the get_all_review_comments (previously called concatenate_all_review_comments) is only used within the volume_of_review_comments method and that modifying volume_of_review_comments to only get a single variable from get_all_review_comments proved to difficult. Unlike the avg_scores_and_count_for_prev_reviews, however, get_all_review_comments was not merged into get_all_review_comments due to its size and that doing so would make get_all_review_comments difficult to understand.&lt;br /&gt;
&lt;br /&gt;
===Issue 2===&lt;br /&gt;
The avg_scores_and_count_for_prev_reviews method has been merged with the significant_difference? method.&lt;br /&gt;
&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    # gets all responses made by a reviewee&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    #&lt;br /&gt;
    count = 0&lt;br /&gt;
    total = 0&lt;br /&gt;
    # gets the sum total percentage scores of all responses that are not this response&lt;br /&gt;
    existing_responses.each do |response|&lt;br /&gt;
      unless id == response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        total +=  response.aggregate_questionnaire_score.to_f / response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    #&lt;br /&gt;
    # if this response is the only response by the reviewee, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    #&lt;br /&gt;
    # calculates the average score of all other responses&lt;br /&gt;
    average_score = total / count&lt;br /&gt;
    #&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    #&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    #&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3===&lt;br /&gt;
We have made the volume_of_review_comments method to be an instance method and moved it to the assignment.rb since it used to take a 'assignment_id' as an input argument. In this way, we are able to make it more sense and removed the ReviewCommentMixin.&lt;br /&gt;
&lt;br /&gt;
  # frozen_string_literal: true&lt;br /&gt;
  module MetricHelper&lt;br /&gt;
    # Determine the average size of review comments in each round&lt;br /&gt;
    # the first entry in the returned list is the overall average&lt;br /&gt;
    # word count.&lt;br /&gt;
    def volume_of_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      comments, counter,&lt;br /&gt;
        @comments_in_round, @counter_in_round = Response.get_all_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      # Index 0 is a nil element that can be ignored in the round count&lt;br /&gt;
      num_rounds = @comments_in_round.count - 1&lt;br /&gt;
  &lt;br /&gt;
      overall_avg_vol = (Lingua::EN::Readability.new(comments).num_words / (counter.zero? ? 1 : counter)).round(0)&lt;br /&gt;
      review_comments_volume = []&lt;br /&gt;
      review_comments_volume.push(overall_avg_vol)&lt;br /&gt;
      (1..num_rounds).each do |round|&lt;br /&gt;
        num = Lingua::EN::Readability.new(@comments_in_round[round]).num_words&lt;br /&gt;
        den = (@counter_in_round[round].zero? ? 1 : @counter_in_round[round])&lt;br /&gt;
        avg_vol_in_round = (num / den).round(0)&lt;br /&gt;
        review_comments_volume.push(avg_vol_in_round)&lt;br /&gt;
      end&lt;br /&gt;
      review_comments_volume&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 4===&lt;br /&gt;
* 1) We have decided not to address this part of the issue (See the Issue 4) after discussing with the professor. The reason behind it is that the lines involved in this section is only 2 lines long and hence it does not have to be moved to a method.&lt;br /&gt;
&lt;br /&gt;
* 2) We looked at a lot of solutions to make this. The issue is that using just a 'where' method will not ensure that the Active Record Association returned will be in the same order of that of the list of ids given.&lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
&lt;br /&gt;
  ids = [4,1,2,7]&lt;br /&gt;
  answers = Answer.where(id: ids)&lt;br /&gt;
  return answers.map(&amp;amp;:id)&lt;br /&gt;
&lt;br /&gt;
This example snippet would return 1,2,4,7&lt;br /&gt;
&lt;br /&gt;
Hence, to ensure the order of the return values(thereby ensuring the correct working) and to improve the performance, we have used a small gem 'find_with_order'. The main reason why we decided to add this gem over a custom solution is portability / ease of modification. A custom solution would have required us to write SQL queries in where clauses and this would be tied to the mysql(current database used). This gem is compatible with popular databases and hence this would be automatically taken care of even if the database is changed.&lt;br /&gt;
&lt;br /&gt;
Hence, our final solution is:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    def calculate_total_score&lt;br /&gt;
      # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
      # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
      sum = 0&lt;br /&gt;
      question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
      # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
      questions = Question.find_with_order(question_ids)&lt;br /&gt;
      scores.each_with_index do |score, idx|&lt;br /&gt;
        sum += score.answer * questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
      end&lt;br /&gt;
      sum&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
  def maximum_score&lt;br /&gt;
    # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
    # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
    total_weight = 0&lt;br /&gt;
    question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
    # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
    questions = Question.find_with_order(question_ids)&lt;br /&gt;
    scores.each_with_index do |score, idx|&lt;br /&gt;
      total_weight += questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
    end&lt;br /&gt;
    questionnaire = if scores.empty?&lt;br /&gt;
                      questionnaire_by_answer(nil)&lt;br /&gt;
                    else&lt;br /&gt;
                      questionnaire_by_answer(scores.first)&lt;br /&gt;
                    end&lt;br /&gt;
    total_weight * questionnaire.max_question_score&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
* 3) Usage of visitor pattern to avoid checking class types: We solved this by adding a method '''response_assignment''' to ReviewResponseMap and ResponseMap classes.&lt;br /&gt;
&lt;br /&gt;
In response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return Participant.find(self.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
In review_response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the review response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return self.assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Hence, the '''questionnaire_by_answer''' method in '''ScorableHelper''' will have the following snippet:&lt;br /&gt;
&lt;br /&gt;
      map = ResponseMap.find(map_id)&lt;br /&gt;
      assignment = map.response_assignment&lt;br /&gt;
&lt;br /&gt;
===Issue 5===&lt;br /&gt;
The get_all_responses method has been made an instance method of response_map and simplified as the function no longer need to find a response map.&lt;br /&gt;
&lt;br /&gt;
  def get_all_responses&lt;br /&gt;
    Response.where(map_id: map_id).all&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
==Design Principles==&lt;br /&gt;
This section aims to act as a consolidation of the design principles that we will implement as part of this project&lt;br /&gt;
* DRY: We will DRY the code that we handle as part of this project. An example of this would be the change that we make as part of the ScorableMixin (see Issue 4).&lt;br /&gt;
&lt;br /&gt;
* Visitor Pattern: Having to check the class of an object and making calls based on that is an example of a bad design pattern. An example of this is ScorableMixin. We will adopt a loose implementation of the Visitor pattern to overcome this by adding a method in ResponseMap and ReviewResponseMap and just call that method in the object we receive. See Issue 4 section for a detailed explanation.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
Since this project is tied to the Response class, all the test cases will be added to response_spec.rb&lt;br /&gt;
&lt;br /&gt;
* Issue 1,2,4: Modify tests in response_spec.rb&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;br /&gt;
* Test comments for review - We also want to test the scenario that involves seeing the comments for a review.&lt;br /&gt;
&lt;br /&gt;
==Future Work==&lt;br /&gt;
The issues pointed out in this documentation will be implemented once the design gets approved. Further changes will be updated in this section as we make progress with the project.&lt;br /&gt;
&lt;br /&gt;
==Mentors==&lt;br /&gt;
* Prof. Edward F. Gehringer&lt;br /&gt;
&lt;br /&gt;
==Contributors==&lt;br /&gt;
* Arvind Srinivas Subramanian (asubram9)&lt;br /&gt;
* David Brain (mdbrain2)&lt;br /&gt;
* Dhanya Sri Dasari (ddasari)&lt;br /&gt;
* Zhihao Wang (zwang238@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150169</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150169"/>
		<updated>2023-04-25T23:12:47Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: /* Issue 3 solved */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
This project is a continuation to E2312. The current project is to make design changes to the Response class, ReviewCommentMixin module, ScorableMixin module and add comments to test cases. We explain the issue with respect to the current implementation of each of these entities as subsections below.&lt;br /&gt;
&lt;br /&gt;
==Note to the reviwer for final submission==&lt;br /&gt;
* This project is a refactoring of some files done in Project 3. We have made these changes in a separate repo so that the new changes can be seen clearly.&lt;br /&gt;
&lt;br /&gt;
* We have added a new section in this documentation called '''Changes made / Solutions''' that has all the changes we have made and explanations. &lt;br /&gt;
&lt;br /&gt;
'''PR for this project:''' https://github.com/expertiza/reimplementation-back-end/pull/38&lt;br /&gt;
&lt;br /&gt;
'''The PR for the previous project (Project 3) on top of which these changes have been made:''' https://github.com/expertiza/reimplementation-back-end/pull/23&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Issues==&lt;br /&gt;
&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactoring significant_difference&lt;br /&gt;
* Make the method in ReviewCommentMixin to be an instance method&lt;br /&gt;
* Simplify + Refactor methods in ScorableMixin&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.  This will allow the client class to use an Iterator pattern to fetch one review at a time from the Response class rather than iterate over that data structure that get_all_review_comments returned to it.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* 1) Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
* 2) Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
* 3) Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
  assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
    map.assignment&lt;br /&gt;
  else&lt;br /&gt;
    Participant.find(map.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
* 1) The common function would involve calculating the sum of weights. &lt;br /&gt;
&lt;br /&gt;
  def calculate_total_weight()&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
  end&lt;br /&gt;
  And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 2) Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
  question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
  questions = Question.where(question_id: question_ids)&lt;br /&gt;
  &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 3) To solve checking the object type, we can write a method named '''response_assignment''' in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return its assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes made / Solutions==&lt;br /&gt;
&lt;br /&gt;
===Issue 1===&lt;br /&gt;
The get_all_review_comments method has been moved to be an instance method of the assignment class and modified to only take the reviewee_id. However, the return value has not been modified. This is because upon further investigation the get_all_review_comments (previously called concatenate_all_review_comments) is only used within the volume_of_review_comments method and that modifying volume_of_review_comments to only get a single variable from get_all_review_comments proved to difficult. Unlike the avg_scores_and_count_for_prev_reviews, however, get_all_review_comments was not merged into get_all_review_comments due to its size and that doing so would make get_all_review_comments difficult to understand.&lt;br /&gt;
&lt;br /&gt;
===Issue 2===&lt;br /&gt;
The avg_scores_and_count_for_prev_reviews method has been merged with the significant_difference? method.&lt;br /&gt;
&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    # gets all responses made by a reviewee&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    #&lt;br /&gt;
    count = 0&lt;br /&gt;
    total = 0&lt;br /&gt;
    # gets the sum total percentage scores of all responses that are not this response&lt;br /&gt;
    existing_responses.each do |response|&lt;br /&gt;
      unless id == response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        total +=  response.aggregate_questionnaire_score.to_f / response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    #&lt;br /&gt;
    # if this response is the only response by the reviewee, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    #&lt;br /&gt;
    # calculates the average score of all other responses&lt;br /&gt;
    average_score = total / count&lt;br /&gt;
    #&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    #&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    #&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3===&lt;br /&gt;
We have made the volume_of_review_comments method to be an instance method and moved it to the assignment.rb since it used to take a 'assignment_id' as an input argument. In this way, we are able to make it more sense and removed the ReviewCommentMixin.&lt;br /&gt;
&lt;br /&gt;
  # frozen_string_literal: true&lt;br /&gt;
  module MetricHelper&lt;br /&gt;
    # Determine the average size of review comments in each round&lt;br /&gt;
    # the first entry in the returned list is the overall average&lt;br /&gt;
    # word count.&lt;br /&gt;
    def volume_of_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      comments, counter,&lt;br /&gt;
        @comments_in_round, @counter_in_round = Response.get_all_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      # Index 0 is a nil element that can be ignored in the round count&lt;br /&gt;
      num_rounds = @comments_in_round.count - 1&lt;br /&gt;
  &lt;br /&gt;
      overall_avg_vol = (Lingua::EN::Readability.new(comments).num_words / (counter.zero? ? 1 : counter)).round(0)&lt;br /&gt;
      review_comments_volume = []&lt;br /&gt;
      review_comments_volume.push(overall_avg_vol)&lt;br /&gt;
      (1..num_rounds).each do |round|&lt;br /&gt;
        num = Lingua::EN::Readability.new(@comments_in_round[round]).num_words&lt;br /&gt;
        den = (@counter_in_round[round].zero? ? 1 : @counter_in_round[round])&lt;br /&gt;
        avg_vol_in_round = (num / den).round(0)&lt;br /&gt;
        review_comments_volume.push(avg_vol_in_round)&lt;br /&gt;
      end&lt;br /&gt;
      review_comments_volume&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 4===&lt;br /&gt;
* 1) We have decided not to address this part of the issue (See the Issue 4) after discussing with the professor. The reason behind it is that the lines involved in this section is only 2 lines long and hence it does not have to be moved to a method.&lt;br /&gt;
&lt;br /&gt;
* 2) We looked at a lot of solutions to make this. The issue is that using just a 'where' method will not ensure that the Active Record Association returned will be in the same order of that of the list of ids given.&lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
&lt;br /&gt;
  ids = [4,1,2,7]&lt;br /&gt;
  answers = Answer.where(id: ids)&lt;br /&gt;
  return answers.map(&amp;amp;:id)&lt;br /&gt;
&lt;br /&gt;
This example snippet would return 1,2,4,7&lt;br /&gt;
&lt;br /&gt;
Hence, to ensure the order of the return values(thereby ensuring the correct working) and to improve the performance, we have used a small gem 'find_with_order'. The main reason why we decided to add this gem over a custom solution is portability / ease of modification. A custom solution would have required us to write SQL queries in where clauses and this would be tied to the mysql(current database used). This gem is compatible with popular databases and hence this would be automatically taken care of even if the database is changed.&lt;br /&gt;
&lt;br /&gt;
Hence, our final solution is:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    def calculate_total_score&lt;br /&gt;
      # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
      # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
      sum = 0&lt;br /&gt;
      question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
      # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
      questions = Question.find_with_order(question_ids)&lt;br /&gt;
      scores.each_with_index do |score, idx|&lt;br /&gt;
        sum += score.answer * questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
      end&lt;br /&gt;
      sum&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
  def maximum_score&lt;br /&gt;
    # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
    # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
    total_weight = 0&lt;br /&gt;
    question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
    # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
    questions = Question.find_with_order(question_ids)&lt;br /&gt;
    scores.each_with_index do |score, idx|&lt;br /&gt;
      total_weight += questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
    end&lt;br /&gt;
    questionnaire = if scores.empty?&lt;br /&gt;
                      questionnaire_by_answer(nil)&lt;br /&gt;
                    else&lt;br /&gt;
                      questionnaire_by_answer(scores.first)&lt;br /&gt;
                    end&lt;br /&gt;
    total_weight * questionnaire.max_question_score&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
* 3) Usage of visitor pattern to avoid checking class types: We solved this by adding a method '''response_assignment''' to ReviewResponseMap and ResponseMap classes.&lt;br /&gt;
&lt;br /&gt;
In response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return Participant.find(self.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
In review_response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the review response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return self.assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Hence, the '''questionnaire_by_answer''' method in '''ScorableHelper''' will have the following snippet:&lt;br /&gt;
&lt;br /&gt;
      map = ResponseMap.find(map_id)&lt;br /&gt;
      assignment = map.response_assignment&lt;br /&gt;
&lt;br /&gt;
===Issue 5===&lt;br /&gt;
The get_all_responses method&lt;br /&gt;
&lt;br /&gt;
==Design Principles==&lt;br /&gt;
This section aims to act as a consolidation of the design principles that we will implement as part of this project&lt;br /&gt;
* DRY: We will DRY the code that we handle as part of this project. An example of this would be the change that we make as part of the ScorableMixin (see Issue 4).&lt;br /&gt;
&lt;br /&gt;
* Visitor Pattern: Having to check the class of an object and making calls based on that is an example of a bad design pattern. An example of this is ScorableMixin. We will adopt a loose implementation of the Visitor pattern to overcome this by adding a method in ResponseMap and ReviewResponseMap and just call that method in the object we receive. See Issue 4 section for a detailed explanation.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
Since this project is tied to the Response class, all the test cases will be added to response_spec.rb&lt;br /&gt;
&lt;br /&gt;
* Issue 1,2,4: Modify tests in response_spec.rb&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;br /&gt;
* Test comments for review - We also want to test the scenario that involves seeing the comments for a review.&lt;br /&gt;
&lt;br /&gt;
==Future Work==&lt;br /&gt;
The issues pointed out in this documentation will be implemented once the design gets approved. Further changes will be updated in this section as we make progress with the project.&lt;br /&gt;
&lt;br /&gt;
==Mentors==&lt;br /&gt;
* Prof. Edward F. Gehringer&lt;br /&gt;
&lt;br /&gt;
==Contributors==&lt;br /&gt;
* Arvind Srinivas Subramanian (asubram9)&lt;br /&gt;
* David Brain (mdbrain2)&lt;br /&gt;
* Dhanya Sri Dasari (ddasari)&lt;br /&gt;
* Zhihao Wang (zwang238@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150168</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150168"/>
		<updated>2023-04-25T23:12:39Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: /* Issue 4 solved */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
This project is a continuation to E2312. The current project is to make design changes to the Response class, ReviewCommentMixin module, ScorableMixin module and add comments to test cases. We explain the issue with respect to the current implementation of each of these entities as subsections below.&lt;br /&gt;
&lt;br /&gt;
==Note to the reviwer for final submission==&lt;br /&gt;
* This project is a refactoring of some files done in Project 3. We have made these changes in a separate repo so that the new changes can be seen clearly.&lt;br /&gt;
&lt;br /&gt;
* We have added a new section in this documentation called '''Changes made / Solutions''' that has all the changes we have made and explanations. &lt;br /&gt;
&lt;br /&gt;
'''PR for this project:''' https://github.com/expertiza/reimplementation-back-end/pull/38&lt;br /&gt;
&lt;br /&gt;
'''The PR for the previous project (Project 3) on top of which these changes have been made:''' https://github.com/expertiza/reimplementation-back-end/pull/23&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Issues==&lt;br /&gt;
&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactoring significant_difference&lt;br /&gt;
* Make the method in ReviewCommentMixin to be an instance method&lt;br /&gt;
* Simplify + Refactor methods in ScorableMixin&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.  This will allow the client class to use an Iterator pattern to fetch one review at a time from the Response class rather than iterate over that data structure that get_all_review_comments returned to it.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* 1) Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
* 2) Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
* 3) Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
  assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
    map.assignment&lt;br /&gt;
  else&lt;br /&gt;
    Participant.find(map.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
* 1) The common function would involve calculating the sum of weights. &lt;br /&gt;
&lt;br /&gt;
  def calculate_total_weight()&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
  end&lt;br /&gt;
  And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 2) Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
  question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
  questions = Question.where(question_id: question_ids)&lt;br /&gt;
  &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 3) To solve checking the object type, we can write a method named '''response_assignment''' in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return its assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes made / Solutions==&lt;br /&gt;
&lt;br /&gt;
===Issue 1===&lt;br /&gt;
The get_all_review_comments method has been moved to be an instance method of the assignment class and modified to only take the reviewee_id. However, the return value has not been modified. This is because upon further investigation the get_all_review_comments (previously called concatenate_all_review_comments) is only used within the volume_of_review_comments method and that modifying volume_of_review_comments to only get a single variable from get_all_review_comments proved to difficult. Unlike the avg_scores_and_count_for_prev_reviews, however, get_all_review_comments was not merged into get_all_review_comments due to its size and that doing so would make get_all_review_comments difficult to understand.&lt;br /&gt;
&lt;br /&gt;
===Issue 2===&lt;br /&gt;
The avg_scores_and_count_for_prev_reviews method has been merged with the significant_difference? method.&lt;br /&gt;
&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    # gets all responses made by a reviewee&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    #&lt;br /&gt;
    count = 0&lt;br /&gt;
    total = 0&lt;br /&gt;
    # gets the sum total percentage scores of all responses that are not this response&lt;br /&gt;
    existing_responses.each do |response|&lt;br /&gt;
      unless id == response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        total +=  response.aggregate_questionnaire_score.to_f / response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    #&lt;br /&gt;
    # if this response is the only response by the reviewee, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    #&lt;br /&gt;
    # calculates the average score of all other responses&lt;br /&gt;
    average_score = total / count&lt;br /&gt;
    #&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    #&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    #&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3 solved===&lt;br /&gt;
We have made the volume_of_review_comments method to be an instance method and moved it to the assignment.rb since it used to take a 'assignment_id' as an input argument. In this way, we are able to make it more sense and removed the ReviewCommentMixin.&lt;br /&gt;
&lt;br /&gt;
  # frozen_string_literal: true&lt;br /&gt;
  module MetricHelper&lt;br /&gt;
    # Determine the average size of review comments in each round&lt;br /&gt;
    # the first entry in the returned list is the overall average&lt;br /&gt;
    # word count.&lt;br /&gt;
    def volume_of_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      comments, counter,&lt;br /&gt;
        @comments_in_round, @counter_in_round = Response.get_all_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      # Index 0 is a nil element that can be ignored in the round count&lt;br /&gt;
      num_rounds = @comments_in_round.count - 1&lt;br /&gt;
  &lt;br /&gt;
      overall_avg_vol = (Lingua::EN::Readability.new(comments).num_words / (counter.zero? ? 1 : counter)).round(0)&lt;br /&gt;
      review_comments_volume = []&lt;br /&gt;
      review_comments_volume.push(overall_avg_vol)&lt;br /&gt;
      (1..num_rounds).each do |round|&lt;br /&gt;
        num = Lingua::EN::Readability.new(@comments_in_round[round]).num_words&lt;br /&gt;
        den = (@counter_in_round[round].zero? ? 1 : @counter_in_round[round])&lt;br /&gt;
        avg_vol_in_round = (num / den).round(0)&lt;br /&gt;
        review_comments_volume.push(avg_vol_in_round)&lt;br /&gt;
      end&lt;br /&gt;
      review_comments_volume&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4===&lt;br /&gt;
* 1) We have decided not to address this part of the issue (See the Issue 4) after discussing with the professor. The reason behind it is that the lines involved in this section is only 2 lines long and hence it does not have to be moved to a method.&lt;br /&gt;
&lt;br /&gt;
* 2) We looked at a lot of solutions to make this. The issue is that using just a 'where' method will not ensure that the Active Record Association returned will be in the same order of that of the list of ids given.&lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
&lt;br /&gt;
  ids = [4,1,2,7]&lt;br /&gt;
  answers = Answer.where(id: ids)&lt;br /&gt;
  return answers.map(&amp;amp;:id)&lt;br /&gt;
&lt;br /&gt;
This example snippet would return 1,2,4,7&lt;br /&gt;
&lt;br /&gt;
Hence, to ensure the order of the return values(thereby ensuring the correct working) and to improve the performance, we have used a small gem 'find_with_order'. The main reason why we decided to add this gem over a custom solution is portability / ease of modification. A custom solution would have required us to write SQL queries in where clauses and this would be tied to the mysql(current database used). This gem is compatible with popular databases and hence this would be automatically taken care of even if the database is changed.&lt;br /&gt;
&lt;br /&gt;
Hence, our final solution is:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    def calculate_total_score&lt;br /&gt;
      # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
      # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
      sum = 0&lt;br /&gt;
      question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
      # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
      questions = Question.find_with_order(question_ids)&lt;br /&gt;
      scores.each_with_index do |score, idx|&lt;br /&gt;
        sum += score.answer * questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
      end&lt;br /&gt;
      sum&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
  def maximum_score&lt;br /&gt;
    # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
    # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
    total_weight = 0&lt;br /&gt;
    question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
    # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
    questions = Question.find_with_order(question_ids)&lt;br /&gt;
    scores.each_with_index do |score, idx|&lt;br /&gt;
      total_weight += questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
    end&lt;br /&gt;
    questionnaire = if scores.empty?&lt;br /&gt;
                      questionnaire_by_answer(nil)&lt;br /&gt;
                    else&lt;br /&gt;
                      questionnaire_by_answer(scores.first)&lt;br /&gt;
                    end&lt;br /&gt;
    total_weight * questionnaire.max_question_score&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
* 3) Usage of visitor pattern to avoid checking class types: We solved this by adding a method '''response_assignment''' to ReviewResponseMap and ResponseMap classes.&lt;br /&gt;
&lt;br /&gt;
In response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return Participant.find(self.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
In review_response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the review response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return self.assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Hence, the '''questionnaire_by_answer''' method in '''ScorableHelper''' will have the following snippet:&lt;br /&gt;
&lt;br /&gt;
      map = ResponseMap.find(map_id)&lt;br /&gt;
      assignment = map.response_assignment&lt;br /&gt;
&lt;br /&gt;
===Issue 5===&lt;br /&gt;
The get_all_responses method&lt;br /&gt;
&lt;br /&gt;
==Design Principles==&lt;br /&gt;
This section aims to act as a consolidation of the design principles that we will implement as part of this project&lt;br /&gt;
* DRY: We will DRY the code that we handle as part of this project. An example of this would be the change that we make as part of the ScorableMixin (see Issue 4).&lt;br /&gt;
&lt;br /&gt;
* Visitor Pattern: Having to check the class of an object and making calls based on that is an example of a bad design pattern. An example of this is ScorableMixin. We will adopt a loose implementation of the Visitor pattern to overcome this by adding a method in ResponseMap and ReviewResponseMap and just call that method in the object we receive. See Issue 4 section for a detailed explanation.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
Since this project is tied to the Response class, all the test cases will be added to response_spec.rb&lt;br /&gt;
&lt;br /&gt;
* Issue 1,2,4: Modify tests in response_spec.rb&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;br /&gt;
* Test comments for review - We also want to test the scenario that involves seeing the comments for a review.&lt;br /&gt;
&lt;br /&gt;
==Future Work==&lt;br /&gt;
The issues pointed out in this documentation will be implemented once the design gets approved. Further changes will be updated in this section as we make progress with the project.&lt;br /&gt;
&lt;br /&gt;
==Mentors==&lt;br /&gt;
* Prof. Edward F. Gehringer&lt;br /&gt;
&lt;br /&gt;
==Contributors==&lt;br /&gt;
* Arvind Srinivas Subramanian (asubram9)&lt;br /&gt;
* David Brain (mdbrain2)&lt;br /&gt;
* Dhanya Sri Dasari (ddasari)&lt;br /&gt;
* Zhihao Wang (zwang238@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150167</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=150167"/>
		<updated>2023-04-25T23:12:28Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: /* Changes made / Solutions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
This project is a continuation to E2312. The current project is to make design changes to the Response class, ReviewCommentMixin module, ScorableMixin module and add comments to test cases. We explain the issue with respect to the current implementation of each of these entities as subsections below.&lt;br /&gt;
&lt;br /&gt;
==Note to the reviwer for final submission==&lt;br /&gt;
* This project is a refactoring of some files done in Project 3. We have made these changes in a separate repo so that the new changes can be seen clearly.&lt;br /&gt;
&lt;br /&gt;
* We have added a new section in this documentation called '''Changes made / Solutions''' that has all the changes we have made and explanations. &lt;br /&gt;
&lt;br /&gt;
'''PR for this project:''' https://github.com/expertiza/reimplementation-back-end/pull/38&lt;br /&gt;
&lt;br /&gt;
'''The PR for the previous project (Project 3) on top of which these changes have been made:''' https://github.com/expertiza/reimplementation-back-end/pull/23&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Issues==&lt;br /&gt;
&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactoring significant_difference&lt;br /&gt;
* Make the method in ReviewCommentMixin to be an instance method&lt;br /&gt;
* Simplify + Refactor methods in ScorableMixin&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.  This will allow the client class to use an Iterator pattern to fetch one review at a time from the Response class rather than iterate over that data structure that get_all_review_comments returned to it.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* 1) Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
* 2) Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
* 3) Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
  assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
    map.assignment&lt;br /&gt;
  else&lt;br /&gt;
    Participant.find(map.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
==== Proposed Solution====&lt;br /&gt;
* 1) The common function would involve calculating the sum of weights. &lt;br /&gt;
&lt;br /&gt;
  def calculate_total_weight()&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
  end&lt;br /&gt;
  And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 2) Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
  question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
  questions = Question.where(question_id: question_ids)&lt;br /&gt;
  &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* 3) To solve checking the object type, we can write a method named '''response_assignment''' in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return its assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Changes made / Solutions==&lt;br /&gt;
&lt;br /&gt;
===Issue 1===&lt;br /&gt;
The get_all_review_comments method has been moved to be an instance method of the assignment class and modified to only take the reviewee_id. However, the return value has not been modified. This is because upon further investigation the get_all_review_comments (previously called concatenate_all_review_comments) is only used within the volume_of_review_comments method and that modifying volume_of_review_comments to only get a single variable from get_all_review_comments proved to difficult. Unlike the avg_scores_and_count_for_prev_reviews, however, get_all_review_comments was not merged into get_all_review_comments due to its size and that doing so would make get_all_review_comments difficult to understand.&lt;br /&gt;
&lt;br /&gt;
===Issue 2===&lt;br /&gt;
The avg_scores_and_count_for_prev_reviews method has been merged with the significant_difference? method.&lt;br /&gt;
&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    # gets all responses made by a reviewee&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    #&lt;br /&gt;
    count = 0&lt;br /&gt;
    total = 0&lt;br /&gt;
    # gets the sum total percentage scores of all responses that are not this response&lt;br /&gt;
    existing_responses.each do |response|&lt;br /&gt;
      unless id == response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        total +=  response.aggregate_questionnaire_score.to_f / response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    #&lt;br /&gt;
    # if this response is the only response by the reviewee, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    #&lt;br /&gt;
    # calculates the average score of all other responses&lt;br /&gt;
    average_score = total / count&lt;br /&gt;
    #&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    #&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    #&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3 solved===&lt;br /&gt;
We have made the volume_of_review_comments method to be an instance method and moved it to the assignment.rb since it used to take a 'assignment_id' as an input argument. In this way, we are able to make it more sense and removed the ReviewCommentMixin.&lt;br /&gt;
&lt;br /&gt;
  # frozen_string_literal: true&lt;br /&gt;
  module MetricHelper&lt;br /&gt;
    # Determine the average size of review comments in each round&lt;br /&gt;
    # the first entry in the returned list is the overall average&lt;br /&gt;
    # word count.&lt;br /&gt;
    def volume_of_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      comments, counter,&lt;br /&gt;
        @comments_in_round, @counter_in_round = Response.get_all_review_comments(assignment_id, reviewer_id)&lt;br /&gt;
      # Index 0 is a nil element that can be ignored in the round count&lt;br /&gt;
      num_rounds = @comments_in_round.count - 1&lt;br /&gt;
  &lt;br /&gt;
      overall_avg_vol = (Lingua::EN::Readability.new(comments).num_words / (counter.zero? ? 1 : counter)).round(0)&lt;br /&gt;
      review_comments_volume = []&lt;br /&gt;
      review_comments_volume.push(overall_avg_vol)&lt;br /&gt;
      (1..num_rounds).each do |round|&lt;br /&gt;
        num = Lingua::EN::Readability.new(@comments_in_round[round]).num_words&lt;br /&gt;
        den = (@counter_in_round[round].zero? ? 1 : @counter_in_round[round])&lt;br /&gt;
        avg_vol_in_round = (num / den).round(0)&lt;br /&gt;
        review_comments_volume.push(avg_vol_in_round)&lt;br /&gt;
      end&lt;br /&gt;
      review_comments_volume&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4 solved===&lt;br /&gt;
* 1) We have decided not to address this part of the issue (See the Issue 4) after discussing with the professor. The reason behind it is that the lines involved in this section is only 2 lines long and hence it does not have to be moved to a method.&lt;br /&gt;
&lt;br /&gt;
* 2) We looked at a lot of solutions to make this. The issue is that using just a 'where' method will not ensure that the Active Record Association returned will be in the same order of that of the list of ids given.&lt;br /&gt;
&lt;br /&gt;
For example:&lt;br /&gt;
&lt;br /&gt;
  ids = [4,1,2,7]&lt;br /&gt;
  answers = Answer.where(id: ids)&lt;br /&gt;
  return answers.map(&amp;amp;:id)&lt;br /&gt;
&lt;br /&gt;
This example snippet would return 1,2,4,7&lt;br /&gt;
&lt;br /&gt;
Hence, to ensure the order of the return values(thereby ensuring the correct working) and to improve the performance, we have used a small gem 'find_with_order'. The main reason why we decided to add this gem over a custom solution is portability / ease of modification. A custom solution would have required us to write SQL queries in where clauses and this would be tied to the mysql(current database used). This gem is compatible with popular databases and hence this would be automatically taken care of even if the database is changed.&lt;br /&gt;
&lt;br /&gt;
Hence, our final solution is:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    def calculate_total_score&lt;br /&gt;
      # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
      # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
      sum = 0&lt;br /&gt;
      question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
      # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
      questions = Question.find_with_order(question_ids)&lt;br /&gt;
      scores.each_with_index do |score, idx|&lt;br /&gt;
        sum += score.answer * questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
      end&lt;br /&gt;
      sum&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
  def maximum_score&lt;br /&gt;
    # Only count the scorable questions, only when the answer is not nil (we accept nil as&lt;br /&gt;
    # answer for scorable questions, and they will not be counted towards the total score)&lt;br /&gt;
    total_weight = 0&lt;br /&gt;
    question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
    # We use find with order here to ensure that the list of questions we get is in the same order as that of question_ids&lt;br /&gt;
    questions = Question.find_with_order(question_ids)&lt;br /&gt;
    scores.each_with_index do |score, idx|&lt;br /&gt;
      total_weight += questions[idx].weight unless score.answer.nil? || !questions[idx].is_a?(ScoredQuestion)&lt;br /&gt;
    end&lt;br /&gt;
    questionnaire = if scores.empty?&lt;br /&gt;
                      questionnaire_by_answer(nil)&lt;br /&gt;
                    else&lt;br /&gt;
                      questionnaire_by_answer(scores.first)&lt;br /&gt;
                    end&lt;br /&gt;
    total_weight * questionnaire.max_question_score&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
* 3) Usage of visitor pattern to avoid checking class types: We solved this by adding a method '''response_assignment''' to ReviewResponseMap and ResponseMap classes.&lt;br /&gt;
&lt;br /&gt;
In response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return Participant.find(self.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
In review_response_map.rb&lt;br /&gt;
&lt;br /&gt;
  # returns the assignment related to the review response map&lt;br /&gt;
  def response_assignment&lt;br /&gt;
    return self.assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Hence, the '''questionnaire_by_answer''' method in '''ScorableHelper''' will have the following snippet:&lt;br /&gt;
&lt;br /&gt;
      map = ResponseMap.find(map_id)&lt;br /&gt;
      assignment = map.response_assignment&lt;br /&gt;
&lt;br /&gt;
===Issue 5===&lt;br /&gt;
The get_all_responses method&lt;br /&gt;
&lt;br /&gt;
==Design Principles==&lt;br /&gt;
This section aims to act as a consolidation of the design principles that we will implement as part of this project&lt;br /&gt;
* DRY: We will DRY the code that we handle as part of this project. An example of this would be the change that we make as part of the ScorableMixin (see Issue 4).&lt;br /&gt;
&lt;br /&gt;
* Visitor Pattern: Having to check the class of an object and making calls based on that is an example of a bad design pattern. An example of this is ScorableMixin. We will adopt a loose implementation of the Visitor pattern to overcome this by adding a method in ResponseMap and ReviewResponseMap and just call that method in the object we receive. See Issue 4 section for a detailed explanation.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
Since this project is tied to the Response class, all the test cases will be added to response_spec.rb&lt;br /&gt;
&lt;br /&gt;
* Issue 1,2,4: Modify tests in response_spec.rb&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;br /&gt;
* Test comments for review - We also want to test the scenario that involves seeing the comments for a review.&lt;br /&gt;
&lt;br /&gt;
==Future Work==&lt;br /&gt;
The issues pointed out in this documentation will be implemented once the design gets approved. Further changes will be updated in this section as we make progress with the project.&lt;br /&gt;
&lt;br /&gt;
==Mentors==&lt;br /&gt;
* Prof. Edward F. Gehringer&lt;br /&gt;
&lt;br /&gt;
==Contributors==&lt;br /&gt;
* Arvind Srinivas Subramanian (asubram9)&lt;br /&gt;
* David Brain (mdbrain2)&lt;br /&gt;
* Dhanya Sri Dasari (ddasari)&lt;br /&gt;
* Zhihao Wang (zwang238@ncsu.edu)&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149229</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149229"/>
		<updated>2023-04-08T01:00:53Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
* Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
&lt;br /&gt;
  scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
* Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
  assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
    map.assignment&lt;br /&gt;
  else&lt;br /&gt;
    Participant.find(map.reviewer_id).assignment&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
* The common function would involve calculating the sum of weight. &lt;br /&gt;
&lt;br /&gt;
  def calculate_total_weight()&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
    question = Question.find(s.question_id)&lt;br /&gt;
    total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
  end&lt;br /&gt;
  And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
  question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
  questions = Question.where(question_id: question_ids)&lt;br /&gt;
  &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* To solve checking the object type, we can write a method in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return it's assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
* Issue 4: Modify tests in response_spec.rb&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149223</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149223"/>
		<updated>2023-04-08T00:55:33Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        scores.each do |s|&lt;br /&gt;
          question = Question.find(s.question_id)&lt;br /&gt;
          &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
        end&lt;br /&gt;
    ```&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
    ```&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
      question = Question.find(s.question_id)&lt;br /&gt;
      &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
    end&lt;br /&gt;
    ```&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
          assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
                         map.assignment&lt;br /&gt;
                       else&lt;br /&gt;
                         Participant.find(map.reviewer_id).assignment&lt;br /&gt;
                       end&lt;br /&gt;
    ```&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
* The common function would involve calculating the sum of weight. &lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        def calculate_total_weight()&lt;br /&gt;
          scores.each do |s|&lt;br /&gt;
          question = Question.find(s.question_id)&lt;br /&gt;
          total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
        end&lt;br /&gt;
    ```&lt;br /&gt;
    And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
        questions = Question.where(question_id: question_ids)&lt;br /&gt;
        &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
    ```&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* To solve checking the object type, we can write a method in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return it's assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
* Issue 4: Modify tests in response_spec.rb&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149221</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149221"/>
		<updated>2023-04-08T00:55:06Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
  def self.avg_scores_and_count_for_prev_reviews(existing_responses, current_response)&lt;br /&gt;
    scores_assigned = []&lt;br /&gt;
    count = 0&lt;br /&gt;
    existing_responses.each do |existing_response|&lt;br /&gt;
      unless existing_response.id == current_response.id # the current_response is also in existing_responses array&lt;br /&gt;
        count += 1&lt;br /&gt;
        scores_assigned &amp;lt;&amp;lt; existing_response.aggregate_questionnaire_score.to_f / existing_response.maximum_score&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    [scores_assigned.sum / scores_assigned.size.to_f, count]&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
  # compare the current response score with other scores on the same artifact, and test if the difference&lt;br /&gt;
  # is significant enough to notify instructor.&lt;br /&gt;
  # Precondition: the response object is associated with a ReviewResponseMap&lt;br /&gt;
  ### &amp;quot;map_class.assessments_for&amp;quot; method need to be refactored&lt;br /&gt;
  def significant_difference?&lt;br /&gt;
    map_class = map.class&lt;br /&gt;
    existing_responses = map_class.assessments_for(map.reviewee)&lt;br /&gt;
    average_score_on_same_artifact_from_others, count = Response.avg_scores_and_count_for_prev_reviews(existing_responses, self)&lt;br /&gt;
    # if this response is the first on this artifact, there's no grade conflict&lt;br /&gt;
    return false if count.zero?&lt;br /&gt;
&lt;br /&gt;
    # This score has already skipped the unfilled scorable question(s)&lt;br /&gt;
    score = aggregate_questionnaire_score.to_f / maximum_score&lt;br /&gt;
    questionnaire = questionnaire_by_answer(scores.first)&lt;br /&gt;
    assignment = map.assignment&lt;br /&gt;
    assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id)&lt;br /&gt;
    # notification_limit can be specified on 'Rubrics' tab on assignment edit page.&lt;br /&gt;
    allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f&lt;br /&gt;
    # the range of average_score_on_same_artifact_from_others and score is [0,1]&lt;br /&gt;
    # the range of allowed_difference_percentage is [0, 100]&lt;br /&gt;
    (average_score_on_same_artifact_from_others - score).abs * 100 &amp;gt; allowed_difference_percentage&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        scores.each do |s|&lt;br /&gt;
          question = Question.find(s.question_id)&lt;br /&gt;
          &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
        end&lt;br /&gt;
    ```&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
    ```&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
      question = Question.find(s.question_id)&lt;br /&gt;
      &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
    end&lt;br /&gt;
    ```&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
          assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
                         map.assignment&lt;br /&gt;
                       else&lt;br /&gt;
                         Participant.find(map.reviewer_id).assignment&lt;br /&gt;
                       end&lt;br /&gt;
    ```&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
* The common function would involve calculating the sum of weight. &lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        def calculate_total_weight()&lt;br /&gt;
          scores.each do |s|&lt;br /&gt;
          question = Question.find(s.question_id)&lt;br /&gt;
          total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
        end&lt;br /&gt;
    ```&lt;br /&gt;
    And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
        questions = Question.where(question_id: question_ids)&lt;br /&gt;
        &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
    ```&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* To solve checking the object type, we can write a method in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return it's assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
A visual representation of this change would look like this. The snippet uses an 'OR' condition to pick the appropriate assignment. Our change would remove the need for this check.&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
* Issue 4: Modify tests in response_spec.rb&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149209</id>
		<title>CSC/ECE 517 Spring 2023 - E2336. Reimplement response.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2336._Reimplement_response.rb&amp;diff=149209"/>
		<updated>2023-04-08T00:46:10Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Project Overview==&lt;br /&gt;
There are several issues we plan to address as part of this project.&lt;br /&gt;
&lt;br /&gt;
* Refactor the get_all_review_comments method as an instance method in response.rb file&lt;br /&gt;
* Refactor the self.get_all_responses method as an instance method in response_map.rb file. &lt;br /&gt;
* Add more descriptive comments for the tests.&lt;br /&gt;
&lt;br /&gt;
===Issue 1: Refactor the self.get_all_review_comments method===&lt;br /&gt;
Currently, the get_all_review_comments method is a class method that returns a data structure containing all the review comments for all responses. This violates the principles of good design as it creates a large data structure that the calling method needs to break apart, leading to poor performance and increased complexity.&lt;br /&gt;
&lt;br /&gt;
[[File:get_all_review_comments.png|700px]]&lt;br /&gt;
&lt;br /&gt;
To address this issue and come up with a much more elegant design, we plan to refactor as a new implementation that should convert the get_all_review_comments method into an instance method that takes a review object as an argument and returns the comments for that review only.&lt;br /&gt;
&lt;br /&gt;
===Issue 2: Refactoring significant_difference?===&lt;br /&gt;
While the significant_difference method does not necessarily need to be refactored, it is the only method with a call for the avg_scores_and_count_for_prev_reviews method, which does need to be refactored it both is a class method and returns a data structure.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Because of this we decided that we should simply refactor significant_difference instead and simply remove avg_scores_and_count_for_prev_reviews.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 3: Make the method in ReviewCommentMixin to be an instance method ===&lt;br /&gt;
====Problems==== &lt;br /&gt;
* The ReviewCommentMixin has one single method '''volume_of_review_comments''' and it is currently present as a class method. There is no need for it to be a class method and it should instead be an instance method. &lt;br /&gt;
* Volume of review comments is a metric, which belongs in metrics.rb, not in a mixin for responses.&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
The change is quite straight forward. We should move this method to be an instance method in '''metrics.rb''' file. A side note is that, this method calls the '''get_all_review_comments''' method from '''response.rb'''. This method will be changed as part of this project (See above points). Any change to that function's method call must be reflected here too.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 4: Simplify + Refactor methods in ScorableMixin ===&lt;br /&gt;
&lt;br /&gt;
This mixin has methods that are related to scoring. https://expertiza.csc.ncsu.edu/index.php/Scoring_%26_Grading_Methods_(Fall_%2721) is a good resource to understand how scoring and grading works.&lt;br /&gt;
&lt;br /&gt;
====Problems====&lt;br /&gt;
* Not DRY : The following piece of code is used in 2 functions - calculate_total_score and maximum_score&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        scores.each do |s|&lt;br /&gt;
          question = Question.find(s.question_id)&lt;br /&gt;
          &amp;lt; Perform a calculation &amp;gt;&lt;br /&gt;
        end&lt;br /&gt;
    ```&lt;br /&gt;
Basically, we iterate through the score and calculate something. The common calculation between both these functions is total_weight calculation.&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' If the logic to calculate the total_weight changes in the future, we will have to make changes to both these places. A developer who is relatively new to the project might miss out one changing it in a location. It is always good to have a single point of change by 'DRY'ing our code.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Not optimized: Another change related to the above snippet has to do with how the queries are made and the number of queries. &lt;br /&gt;
    ```&lt;br /&gt;
    scores.each do |s|&lt;br /&gt;
      question = Question.find(s.question_id)&lt;br /&gt;
      &amp;lt;Other lines irrelevant to this case &amp;gt;&lt;br /&gt;
    end&lt;br /&gt;
    ```&lt;br /&gt;
&lt;br /&gt;
'''What problem does this cause?''' This is done inside a loop, meaning we perform a select query to each time -&amp;gt; N queries for N objects.  Our solution aims to optimize this.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Checking the class type of an object to perform some business logic&lt;br /&gt;
&lt;br /&gt;
This issue is present in the questionnaire_by_answer method. The following snippet shows it:&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
          assignment = if map.is_a? ReviewResponseMap&lt;br /&gt;
                         map.assignment&lt;br /&gt;
                       else&lt;br /&gt;
                         Participant.find(map.reviewer_id).assignment&lt;br /&gt;
                       end&lt;br /&gt;
    ```&lt;br /&gt;
&lt;br /&gt;
====Solutions====&lt;br /&gt;
* The common function would involve calculating the sum of weight. &lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        def calculate_total_weight()&lt;br /&gt;
          scores.each do |s|&lt;br /&gt;
          question = Question.find(s.question_id)&lt;br /&gt;
          total_weight += question.weight unless s.answer.nil? || !question.is_a?(ScoredQuestion)&lt;br /&gt;
        end&lt;br /&gt;
    ```&lt;br /&gt;
    And this can be called inside the calculate_total_score and maximum_score.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Instead of calling Question.find inside the loop, we can get the set of question_ids and use a where function.&lt;br /&gt;
&lt;br /&gt;
    ```&lt;br /&gt;
        question_ids = scores.map(&amp;amp;:question_id)&lt;br /&gt;
        questions = Question.where(question_id: question_ids)&lt;br /&gt;
        &amp;lt; Make the calculation here &amp;gt;&lt;br /&gt;
    ```&lt;br /&gt;
  This way, we only make one database call instead of N calls.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* To solve checking the object type, we can write a method in ResponseMap and ReviewResponseMap. This way, we can just call that method in the object and depending on what object it is, we would get the corresponding answer.&lt;br /&gt;
   * In ReviewResponseMap, the method should return it's assignment&lt;br /&gt;
   * In ResponseMap, the method should return Participant.find(map.reviewer_id).assignment&lt;br /&gt;
&lt;br /&gt;
[[File:ScorableMixin2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Issue 5: Refactor the self.get_all_responses method===&lt;br /&gt;
&lt;br /&gt;
The self.get_all_responses method which is currently in the response.rb file would be much cleaner as an instance method in response_map.rb. We plan to refactor the self.get_all_responses method in response.rb to an instance method in response_map.rb.&lt;br /&gt;
&lt;br /&gt;
[[File:Get all responses.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response map class.png|700px]]&lt;br /&gt;
&lt;br /&gt;
===Issue 6: Add more descriptive comments for the tests===&lt;br /&gt;
&lt;br /&gt;
The existing tests need more descriptive comments. We plan to add more detailed comments for all the test cases.&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 1.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 2.png|700px]]&lt;br /&gt;
&lt;br /&gt;
[[File:Response tests 3.png|700px]]&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
As part of this section, we track the set of files that are to be changed for tests and scenarios that are to be tested.&lt;br /&gt;
===Files===&lt;br /&gt;
* Issue 3: Write tests for '''volume_of_review_comments''' method&lt;br /&gt;
* Issue 4: Modify tests in response_spec.rb&lt;br /&gt;
===Scenarios===&lt;br /&gt;
* Test the score calculation cases - maximum possible total score for all scores, average score across all of the instances and total score (weighted sum). Ideally average score calculation should cover all the scenarios, that is, if the maximum score is zero, it uses the total score(weighted).&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2312_%2B_E2313._Reimplement_response.rb_and_responses_controller.rb&amp;diff=148610</id>
		<title>CSC/ECE 517 Spring 2023 - E2312 + E2313. Reimplement response.rb and responses controller.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2312_%2B_E2313._Reimplement_response.rb_and_responses_controller.rb&amp;diff=148610"/>
		<updated>2023-03-28T03:06:44Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
In order to accommodate issues with another team, our team picked up another member and did both projects. The summaries of the projects are combined here to reflect that. Note that the requirements on the original projects are being treated more loosely to account for the additional work.&lt;br /&gt;
&lt;br /&gt;
= E2312 Reimplement Response =&lt;br /&gt;
&lt;br /&gt;
== Summary of Changes ==&lt;br /&gt;
&lt;br /&gt;
The previous version of response.rb was described as &amp;quot;a mess.&amp;quot; The goal of E2312 was to reimplement it to follow better coding standards.&lt;br /&gt;
&lt;br /&gt;
* Made method names clearer, many method names were opaque and did not properly convey their functionality. For example, add_table_rows:&lt;br /&gt;
[[File:add_table_rows.png]]&lt;br /&gt;
* Moved many functionalities to mixins or helper classes to not violate the Single-Responsibility Principle (also many comments with no useful information)&lt;br /&gt;
[[File:scores_response.png]]&lt;br /&gt;
* Split the avg_scores_and_count_for_prev_reviews method into two separate methods&lt;br /&gt;
* Implemented additional functionally&lt;br /&gt;
&lt;br /&gt;
== Functionality Moved ==&lt;br /&gt;
&lt;br /&gt;
This section covers the functionality that was moved out of the response model.&lt;br /&gt;
&lt;br /&gt;
=== Score Calculation ===&lt;br /&gt;
&lt;br /&gt;
Score calculation is not inherently tied to the idea of a response.&lt;br /&gt;
Any object with multiple scores could take advantage of a mixin that provided methods to do calculations on those scores. This created multiple responsibilities for the response object and encouraged non-DRY code.&lt;br /&gt;
Functionality for calculating total, average, and maximum scores were all moved to a new Scorable mixin which can be leveraged by other models moving forward.&lt;br /&gt;
&lt;br /&gt;
Previously that functionality was implemented directly on the response class:&lt;br /&gt;
&lt;br /&gt;
[[File:aggregate_score.png]]&lt;br /&gt;
&lt;br /&gt;
Now it exists in a mixin, as demonstrated with this snippet:&lt;br /&gt;
&lt;br /&gt;
[[File:scorable_mixin.png]]&lt;br /&gt;
&lt;br /&gt;
=== Emails ===&lt;br /&gt;
&lt;br /&gt;
The response model provided a method for creating emails. This was moved to a new MailMixin, which can be expanded to provide email functionality to more models in the future. This removed additional responsibilities to help maintain the Single Responsibility principle and helps move email-based code toward a more DRY implementation in the future.&lt;br /&gt;
&lt;br /&gt;
[[File:mail_mixin.png]]&lt;br /&gt;
&lt;br /&gt;
=== Metrics ===&lt;br /&gt;
&lt;br /&gt;
The response model provided a method for getting the volume of review comments on a response. This was moved to a ReviewCommentMixin as it is not necessarily specific to response models. This helps maintain the Single-Responsibility Principle for the response model.&lt;br /&gt;
&lt;br /&gt;
[[File:review_comment.png]]&lt;br /&gt;
&lt;br /&gt;
== Improved Naming ==&lt;br /&gt;
&lt;br /&gt;
Fix instances of bad and opaque naming, like aggregate_question_score(), add_table_row(), and concatenate_all_review_comments() (which does more than the name implies).&lt;br /&gt;
&lt;br /&gt;
== Method Split ==&lt;br /&gt;
&lt;br /&gt;
The method avg_scores_and_count_for_prev_reviews was split into two separate functions, prev_reviews_count and prev_reviews_avg_scores.&lt;br /&gt;
&lt;br /&gt;
== Additional Functionality ==&lt;br /&gt;
&lt;br /&gt;
Two methods were added, get_latest_response and get_all_responses. These two methods both get responses made by a particular reviewer for a particular reviewee for an assignment. Get_latest_response returns the latest of the responses and get_all_responses returns all of the responses.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
Many other parts of the software were stubbed out in the implementation project in order to run unit tests. The existing test suite was run against the implementation and passed successfully. Due to the fact that this project has no front end, the professor asked us to record a video of tests being run to demonstrate functionality, which is linked in the project submission.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Test cases:&lt;br /&gt;
&lt;br /&gt;
* Total score of a review&lt;br /&gt;
* Average scores across a review when the maximum score is 0&lt;br /&gt;
* Average scores across a review when the maximum score is not 0&lt;br /&gt;
* Maximum scores for a response when there are no points available&lt;br /&gt;
* Maximum scores for a response standard case&lt;br /&gt;
* Volume of review comments&lt;br /&gt;
* Concatenating all review comments&lt;br /&gt;
&lt;br /&gt;
= E2313 Reimplement Response Controller =&lt;br /&gt;
&lt;br /&gt;
== Summary of Changes ==&lt;br /&gt;
&lt;br /&gt;
The previous version of response_controller.rb was too long. It included too many functions besides the basic CRUD methods. The goal of E2313 was to reimplement it to follow better coding standards.&lt;br /&gt;
&lt;br /&gt;
* Made method name to follow plural convention not singular convention. &lt;br /&gt;
* Moved many functionalities to mixins or helper classes&lt;br /&gt;
&lt;br /&gt;
== Functionality Moved ==&lt;br /&gt;
&lt;br /&gt;
This section covers the functionality that was moved out of the responses controller.&lt;br /&gt;
&lt;br /&gt;
=== Authentication and Authorization ===&lt;br /&gt;
&lt;br /&gt;
There were authentication and authorization methods in the response controller. These were moved to authorization helper.&lt;br /&gt;
&lt;br /&gt;
=== Redirect ===&lt;br /&gt;
&lt;br /&gt;
There was a redirect function that helps redirect by params. It is moved to response helper.&lt;br /&gt;
&lt;br /&gt;
=== Sort Reviews ===&lt;br /&gt;
&lt;br /&gt;
There was functionality to sort reviews. We made a new function named sortReviews to perform this functionality and placed it in the response model&lt;br /&gt;
&lt;br /&gt;
== Singular convention to Plural convention ==&lt;br /&gt;
&lt;br /&gt;
There were some function and classes that followed singular conventions, but were changed to follow plural convention.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:plural.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:authorization_helper.png]]&lt;br /&gt;
&lt;br /&gt;
== Reducing code ==&lt;br /&gt;
&lt;br /&gt;
There were multiple method invocations to set parameters (assign_action_parameters, set_content). We used the macro, before_action, so that we reduced the number of calls for these methods.&lt;br /&gt;
&lt;br /&gt;
[[File:macro.png]]&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
1. Response Deletion&lt;br /&gt;
&lt;br /&gt;
 - Verify that if the response is enabled for team reviewing, but lock is not existent, proceed to lock response&lt;br /&gt;
 - Verify that if the response is not team review enabled, proceed to delete the response and redirect the user&lt;br /&gt;
&lt;br /&gt;
2. Response Edit&lt;br /&gt;
 &lt;br /&gt;
 - Verify preparing already existent response for editing purposes&lt;br /&gt;
 - Verify creating new response if non-existent for editing purposes&lt;br /&gt;
&lt;br /&gt;
3. Response Update&lt;br /&gt;
 &lt;br /&gt;
 - Verify submitted response contains the new updated fields upon submission&lt;br /&gt;
&lt;br /&gt;
4. Response New Feedback&lt;br /&gt;
&lt;br /&gt;
 - Redirect user to the new feedback page with relevant Response, Participant, and FeedbackResponseMap data&lt;br /&gt;
 - If a review is not found, redirect user to the root path&lt;br /&gt;
&lt;br /&gt;
5. Response Create&lt;br /&gt;
&lt;br /&gt;
 - If Response is found by query parameter, update with additional comments and instantiate answers&lt;br /&gt;
 - If Response is not found by query parameter, create new Response instance with additional comments and instantiate answers&lt;br /&gt;
&lt;br /&gt;
6. Response Save&lt;br /&gt;
&lt;br /&gt;
 - With the found ResponseMap, save with existing query parameters&lt;br /&gt;
&lt;br /&gt;
7. Initialize Answers&lt;br /&gt;
&lt;br /&gt;
 - Verify answers are created from a passed-in list of questions&lt;br /&gt;
&lt;br /&gt;
8. Questionnaire from Response Map&lt;br /&gt;
&lt;br /&gt;
 - Verify if current map type is ReviewResponseMap, set current round and questionnaire accordingly&lt;br /&gt;
 - Verify if current map type is SelfReviewResponseMap, set current round and questionnaire accordingly&lt;br /&gt;
 - Verify if current map type is MetareviewResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is TeammateReviewResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is FeedbackResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is CourseSurveyResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is AssignmentSurveyResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is GlobalSurveyResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is BookmarkRatingResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
&lt;br /&gt;
9. Assign Action Parameters&lt;br /&gt;
&lt;br /&gt;
 - Verify when action is edit, header and next_action are assigned as &amp;quot;Edit&amp;quot; and &amp;quot;update&amp;quot; respectively&lt;br /&gt;
 - Verify when action is new, header and next_action are assigned as &amp;quot;New&amp;quot; and &amp;quot;create&amp;quot; respectively&lt;br /&gt;
&lt;br /&gt;
10. Setting Response&lt;br /&gt;
&lt;br /&gt;
 - Verify response and map instances are set based on the query parameter passed in&lt;br /&gt;
&lt;br /&gt;
11. Toggling permission&lt;br /&gt;
&lt;br /&gt;
 - Verify a given response is found and its visibility attribute is toggled to its corresponding opposite value&lt;br /&gt;
&lt;br /&gt;
12. Show Calibration Results for Student&lt;br /&gt;
&lt;br /&gt;
 - Verify with the given query parameters: assignment_id, calibration_response_map_id, review_response_map_id, that the assignment, calibration response, review response and questions instances are set correctly&lt;br /&gt;
&lt;br /&gt;
13. Previous Review Count&lt;br /&gt;
&lt;br /&gt;
 - Verify that when given a list of reviews and the current review that the number of non current reviews is accurate&lt;br /&gt;
&lt;br /&gt;
14. Previous Review Average Score&lt;br /&gt;
&lt;br /&gt;
 - Verify that when given a list of reviews and the current review that the average score of all non current reviews is accurate&lt;br /&gt;
&lt;br /&gt;
15. Gets Latest Response by a Reviewer&lt;br /&gt;
&lt;br /&gt;
 - Verify that the latest response by a reviewer given the assignment, reviewer, and reviewee is accurate&lt;br /&gt;
&lt;br /&gt;
16. Gets All Responses by a Reviewer&lt;br /&gt;
&lt;br /&gt;
 - Verify that all responses by a reviewer given the assignment, reviewer, and reviewee is accurate&lt;br /&gt;
&lt;br /&gt;
[[File:Screenshot 2023-03-27 at 10.40.26 PM.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:Answers.png]]&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2312_%2B_E2313._Reimplement_response.rb_and_responses_controller.rb&amp;diff=148605</id>
		<title>CSC/ECE 517 Spring 2023 - E2312 + E2313. Reimplement response.rb and responses controller.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2312_%2B_E2313._Reimplement_response.rb_and_responses_controller.rb&amp;diff=148605"/>
		<updated>2023-03-28T02:57:47Z</updated>

		<summary type="html">&lt;p&gt;Mdbrain2: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
In order to accommodate issues with another team, our team picked up another member and did both projects. The summaries of the projects are combined here to reflect that. Note that the requirements on the original projects are being treated more loosely to account for the additional work.&lt;br /&gt;
&lt;br /&gt;
= E2312 Reimplement Response =&lt;br /&gt;
&lt;br /&gt;
== Summary of Changes ==&lt;br /&gt;
&lt;br /&gt;
The previous version of response.rb was described as &amp;quot;a mess.&amp;quot; The goal of E2312 was to reimplement it to follow better coding standards.&lt;br /&gt;
&lt;br /&gt;
* Made method names clearer, many method names were opaque and did not properly convey their functionality. For example, add_table_rows:&lt;br /&gt;
[[File:add_table_rows.png]]&lt;br /&gt;
* Moved many functionalities to mixins or helper classes to not violate the Single-Responsibility Principle (also many comments with no useful information)&lt;br /&gt;
[[File:scores_response.png]]&lt;br /&gt;
* Reduced usage of class methods&lt;br /&gt;
&lt;br /&gt;
== Functionality Moved ==&lt;br /&gt;
&lt;br /&gt;
This section covers the functionality that was moved out of the response model.&lt;br /&gt;
&lt;br /&gt;
=== Score Calculation ===&lt;br /&gt;
&lt;br /&gt;
Score calculation is not inherently tied to the idea of a response.&lt;br /&gt;
Any object with multiple scores could take advantage of a mixin that provided methods to do calculations on those scores. This created multiple responsibilities for the response object and encouraged non-DRY code.&lt;br /&gt;
Functionality for calculating total, average, and maximum scores were all moved to a new Scorable mixin which can be leveraged by other models moving forward.&lt;br /&gt;
&lt;br /&gt;
Previously that functionality was implemented directly on the response class:&lt;br /&gt;
&lt;br /&gt;
[[File:aggregate_score.png]]&lt;br /&gt;
&lt;br /&gt;
Now it exists in a mixin, as demonstrated with this snippet:&lt;br /&gt;
&lt;br /&gt;
[[File:scorable_mixin.png]]&lt;br /&gt;
&lt;br /&gt;
=== Emails ===&lt;br /&gt;
&lt;br /&gt;
The response model provided a method for creating emails. This was moved to a new MailMixin, which can be expanded to provide email functionality to more models in the future. This removed additional responsibilities to help maintain the Single Responsibility principle and helps move email-based code toward a more DRY implementation in the future.&lt;br /&gt;
&lt;br /&gt;
[[File:mail_mixin.png]]&lt;br /&gt;
&lt;br /&gt;
=== Metrics ===&lt;br /&gt;
&lt;br /&gt;
The response model provided a method for getting the volume of review comments on a response. This was moved to a ReviewCommentMixin as it is not necessarily specific to response models. This helps maintain the Single-Responsibility Principle for the response model.&lt;br /&gt;
&lt;br /&gt;
[[File:review_comment.png]]&lt;br /&gt;
&lt;br /&gt;
== Improved Naming ==&lt;br /&gt;
&lt;br /&gt;
Fix instances of bad and opaque naming, like aggregate_question_score(), add_table_row(), and concatenate_all_review_comments() (which does more than the name implies).&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
Many other parts of the software were stubbed out in the implementation project in order to run unit tests. The existing test suite was run against the implementation and passed successfully. Due to the fact that this project has no front end, the professor asked us to record a video of tests being run to demonstrate functionality, which is linked in the project submission.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Test cases:&lt;br /&gt;
&lt;br /&gt;
* Total score of a review&lt;br /&gt;
* Average scores across a review when the maximum score is 0&lt;br /&gt;
* Average scores across a review when the maximum score is not 0&lt;br /&gt;
* Maximum scores for a response when there are no points available&lt;br /&gt;
* Maximum scores for a response standard case&lt;br /&gt;
* Volume of review comments&lt;br /&gt;
* Concatenating all review comments&lt;br /&gt;
&lt;br /&gt;
= E2313 Reimplement Response Controller =&lt;br /&gt;
&lt;br /&gt;
== Summary of Changes ==&lt;br /&gt;
&lt;br /&gt;
The previous version of response_controller.rb was too long. It included too many functions besides the basic CRUD methods. The goal of E2313 was to reimplement it to follow better coding standards.&lt;br /&gt;
&lt;br /&gt;
* Made method name to follow plural convention not singular convention. &lt;br /&gt;
* Moved many functionalities to mixins or helper classes&lt;br /&gt;
&lt;br /&gt;
== Functionality Moved ==&lt;br /&gt;
&lt;br /&gt;
This section covers the functionality that was moved out of the responses controller.&lt;br /&gt;
&lt;br /&gt;
=== Authentication and Authorization ===&lt;br /&gt;
&lt;br /&gt;
There were authentication and authorization methods in the response controller. These were moved to authorization helper.&lt;br /&gt;
&lt;br /&gt;
=== Redirect ===&lt;br /&gt;
&lt;br /&gt;
There was a redirect function that helps redirect by params. It is moved to response helper.&lt;br /&gt;
&lt;br /&gt;
=== Sort Reviews ===&lt;br /&gt;
&lt;br /&gt;
There was functionality to sort reviews. We made a new function named sortReviews to perform this functionality and placed it in the response model&lt;br /&gt;
&lt;br /&gt;
== Singular convention to Plural convention ==&lt;br /&gt;
&lt;br /&gt;
There were some function and classes that followed singular conventions, but were changed to follow plural convention.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:plural.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:authorization_helper.png]]&lt;br /&gt;
&lt;br /&gt;
== Reducing code ==&lt;br /&gt;
&lt;br /&gt;
There were multiple method invocations to set parameters (assign_action_parameters, set_content). We used the macro, before_action, so that we reduced the number of calls for these methods.&lt;br /&gt;
&lt;br /&gt;
[[File:macro.png]]&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
&lt;br /&gt;
1. Response Deletion&lt;br /&gt;
&lt;br /&gt;
 - Verify that if the response is enabled for team reviewing, but lock is not existent, proceed to lock response&lt;br /&gt;
 - Verify that if the response is not team review enabled, proceed to delete the response and redirect the user&lt;br /&gt;
&lt;br /&gt;
2. Response Edit&lt;br /&gt;
 &lt;br /&gt;
 - Verify preparing already existent response for editing purposes&lt;br /&gt;
 - Verify creating new response if non-existent for editing purposes&lt;br /&gt;
&lt;br /&gt;
3. Response Update&lt;br /&gt;
 &lt;br /&gt;
 - Verify submitted response contains the new updated fields upon submission&lt;br /&gt;
&lt;br /&gt;
4. Response New Feedback&lt;br /&gt;
&lt;br /&gt;
 - Redirect user to the new feedback page with relevant Response, Participant, and FeedbackResponseMap data&lt;br /&gt;
 - If a review is not found, redirect user to the root path&lt;br /&gt;
&lt;br /&gt;
5. Response Create&lt;br /&gt;
&lt;br /&gt;
 - If Response is found by query parameter, update with additional comments and instantiate answers&lt;br /&gt;
 - If Response is not found by query parameter, create new Response instance with additional comments and instantiate answers&lt;br /&gt;
&lt;br /&gt;
6. Response Save&lt;br /&gt;
&lt;br /&gt;
 - With the found ResponseMap, save with existing query parameters&lt;br /&gt;
&lt;br /&gt;
7. Initialize Answers&lt;br /&gt;
&lt;br /&gt;
 - Verify answers are created from a passed-in list of questions&lt;br /&gt;
&lt;br /&gt;
8. Questionnaire from Response Map&lt;br /&gt;
&lt;br /&gt;
 - Verify if current map type is ReviewResponseMap, set current round and questionnaire accordingly&lt;br /&gt;
 - Verify if current map type is SelfReviewResponseMap, set current round and questionnaire accordingly&lt;br /&gt;
 - Verify if current map type is MetareviewResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is TeammateReviewResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is FeedbackResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is CourseSurveyResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is AssignmentSurveyResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is GlobalSurveyResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
 - Verify if current map type is BookmarkRatingResponseMap, set questionnaire based on if assignment is duty based&lt;br /&gt;
&lt;br /&gt;
9. Assign Action Parameters&lt;br /&gt;
&lt;br /&gt;
 - Verify when action is edit, header and next_action are assigned as &amp;quot;Edit&amp;quot; and &amp;quot;update&amp;quot; respectively&lt;br /&gt;
 - Verify when action is new, header and next_action are assigned as &amp;quot;New&amp;quot; and &amp;quot;create&amp;quot; respectively&lt;br /&gt;
&lt;br /&gt;
10. Setting Response&lt;br /&gt;
&lt;br /&gt;
 - Verify response and map instances are set based on the query parameter passed in&lt;br /&gt;
&lt;br /&gt;
11. Toggling permission&lt;br /&gt;
&lt;br /&gt;
 - Verify a given response is found and its visibility attribute is toggled to its corresponding opposite value&lt;br /&gt;
&lt;br /&gt;
12. Show Calibration Results for Student&lt;br /&gt;
&lt;br /&gt;
 - Verify with the given query parameters: assignment_id, calibration_response_map_id, review_response_map_id, that the assignment, calibration response, review response and questions instances are set correctly&lt;br /&gt;
&lt;br /&gt;
13. Previous Review Count&lt;br /&gt;
&lt;br /&gt;
 - Verify that when given a list of reviews and the current review that the number of non current reviews is accurate&lt;br /&gt;
&lt;br /&gt;
14. Previous Review Average Score&lt;br /&gt;
&lt;br /&gt;
 - Verify that when given a list of reviews and the current review that the average score of all non current reviews is accurate&lt;br /&gt;
&lt;br /&gt;
15. Gets Latest Response by a Reviewer&lt;br /&gt;
&lt;br /&gt;
 - Verify that the latest response by a reviewer given the assignment, reviewer, and reviewee is accurate&lt;br /&gt;
&lt;br /&gt;
16. Gets All Responses by a Reviewer&lt;br /&gt;
&lt;br /&gt;
 - Verify that all responses by a reviewer given the assignment, reviewer, and reviewee is accurate&lt;br /&gt;
&lt;br /&gt;
[[File:Screenshot 2023-03-27 at 10.40.26 PM.png]]&lt;br /&gt;
&lt;br /&gt;
[[File:Answers.png]]&lt;/div&gt;</summary>
		<author><name>Mdbrain2</name></author>
	</entry>
</feed>