CSC/ECE 517 Spring 2025 - E2512. Reimplement responses controller

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

The ResponseController in Expertiza is responsible for handling responses to quizzes, peer reviews, and surveys. However, the current implementation has several design issues, including violations of Rails naming conventions, excessive responsibilities, and redundant logic. The goal of this project is to reimplement (not refactor) a new response_controller.rb as responses_controller.rb. This includes CRUD operations (create, read, update, delete) and the functionality previously found in the old controller, but done in a clean, modern, and conventional way

Motivation

The Responses controller currently does not follow modern idioms and the updated Expertiza style guide. It also contains too many extra pieces of logic and needs to move logic to other mailers/helpers/services/models.

Tasks Identified

  • Rename response_controller.rb to responses_controller.rb to adhere to Rails naming conventions.
  • Limit the controller to CRUD operations, moving non-CRUD logic to models, helpers, or services.
  • Ensure questions (items) are displayed in the correct sequence when rendering responses.
  • Move the sorting logic from the edit method to the Response model for better organization.
  • Rename sort_questions to sort_items for improved clarity.
  • Use polymorphism to determine if the reviewer is a team, replacing existing if statements.
  • Consolidate redundant parameter-setting methods (assign_action_parameters, set_content) into a single, more efficient method.
  • Refactor questionnaire_from_response_map to simplify logic and support topic-based rubrics.
  • Identify and remove unused (dead) methods to clean up the codebase.
  • Extract email generation logic from the controller and move it to a dedicated helper.

Our Reimplementation

Move Sorting Logic & Display In Correct Order

Previously, the sorting logic was inside the update method of the Response controller:

 if @prev.present?
     @sorted = @review_scores.sort do |m1, m2|
       if m1.version_num.to_i && m2.version_num.to_i
         m2.version_num.to_i <=> m1.version_num.to_i
       else
         m1.version_num ? -1 : 1
       end
     end
     @largest_version_num = @sorted[0]
   end

This was re-implemented in the Response model:

 # Sort responses by version number, descending
 def self.sort_by_version
   review_scores = Response.where(map_id: @map.id).to_a
   return [] if review_scores.empty?
   sorted = review_scores.sort_by { |response| response.version_num.to_i }.reverse
   sorted[0]
 end

Similarly, sort_questions was re-implemented in ResponsesHelper:

 #Renamed to sort_items from sort_questions
 def sort_items(questions)
   questions.sort_by(&:seq)
 end

Consolidate Parameter Setting Methods

The old ResponseController had separate methods to set key Response parameters: assign_action_parameters and set_content. These methods are highly duplicative of each other and add more complexity to the code base. They are also non-CRUD methods in the controller, so we re-implemented them into one helper method: prepare_response_content

def prepare_response_content(map, action_params = nil, new_response = false)

       # Set title and other initial content based on the map
       title = map.get_title
       survey_parent = nil
       assignment = nil
       participant = map.reviewer
       contributor = map.contributor
 
       if map.survey?
         survey_parent = map.survey_parent
       else
         assignment = map.assignment
       end
 
       # Get the questionnaire and sort questions
       questionnaire = questionnaire_from_response_map(map, contributor, assignment)
       review_questions = Response.sort_by_version(questionnaire.questions)
       min = questionnaire.min_question_score
       max = questionnaire.max_question_score
       # Initialize response if new_response is true
       response = nil
       if new_response
         response = Response.where(map_id: map.id).order(updated_at: :desc).first
         if response.nil?
           response = Response.create(map_id: map.id, additional_comment: , is_submitted: 0)
         end
       end
       # Set up dropdowns or scales
       set_dropdown_or_scale(questionnaire, assignment)
       # Process the action parameters if provided
       if action_params
         case action_params[:action]
         when 'edit'
           header = 'Edit'
           next_action = 'update'
           response = Response.find(action_params[:id])
           contributor = map.contributor
         when 'new'
           header = 'New'
           next_action = 'create'
           feedback = action_params[:feedback]
           modified_object = map.id
         end
       end 

By using a few parameters to manage cases around the action being 'new', we were able to combine these methods into one method.

Refactor Questionnaire_from_response_map

The old questionnaire_from_response_map method did not support topic based reviewing and resided in the controller:

 def questionnaire_from_response_map
   case @map.type
   when 'ReviewResponseMap', 'SelfReviewResponseMap'
     reviewees_topic = SignedUpTeam.topic_id_by_team_id(@contributor.id)
     @current_round = @assignment.number_of_current_round(reviewees_topic)
     @questionnaire = @map.questionnaire(@current_round, reviewees_topic)
   when
     'MetareviewResponseMap',
     'TeammateReviewResponseMap',
     'FeedbackResponseMap',
     'CourseSurveyResponseMap',
     'AssignmentSurveyResponseMap',
     'GlobalSurveyResponseMap',
     'BookmarkRatingResponseMap'
     if @assignment.duty_based_assignment?
       # E2147 : gets questionnaire of a particular duty in that assignment rather than generic questionnaire
       @questionnaire = @map.questionnaire_by_duty(@map.reviewee.duty_id)
     else
       @questionnaire = @map.questionnaire
     end
   end
 end

To remedy this, the method was re-implemented in the ResponsesHelper file, along with some helper methods :

 def questionnaire_from_response_map(map, contributor, assignment)
   if ['ReviewResponseMap', 'SelfReviewResponseMap'].include?(map.type)
     get_questionnaire_by_contributor(map, contributor, assignment)
   else
     get_questionnaire_by_duty(map, assignment)
   end
   end
   def get_questionnaire_by_contributor(map, contributor, assignment)       
     reviewees_topic = SignedUpTeam.find_by(team_id: contributor.id)&.sign_up_topic_id
     current_round = DueDate.next_due_date(reviewees_topic).round
     map.questionnaire(current_round, reviewees_topic)
   end
   def get_questionnaire_by_duty(map, assignment)
     if assignment.duty_based_assignment?
       # E2147 : gets questionnaire of a particular duty in that assignment rather than generic questionnaire
       map.questionnaire_by_duty(map.reviewee.duty_id)
   else
       map.questionnaire
   end
 end

Extract email generation logic and move to model

Previously, the email generation logic resided in the controller and was designed around one type of email:

 def send_email
   subject = params['send_email']['subject']
   body = params['send_email']['email_body']
   response = params['response']
   email = params['email']
   respond_to do |format|
     if subject.blank? || body.blank?
       flash[:error] = 'Please fill in the subject and the email content.'
       format.html { redirect_to controller: 'response', action: 'author', response: response, email: email }
       format.json { head :no_content }
     else
       # make a call to method invoking the email process
       MailerHelper.send_mail_to_author_reviewers(subject, body, email)
       flash[:success] = 'Email sent to the author.'
       format.html { redirect_to controller: 'student_task', action: 'list' }
       format.json { head :no_content }
     end
   end
 end

This was re-implemented in the ResponseMailer class:

   # Send an email to authors from a reviewer
   def send_response_email(response)
       @body = response.params[:send_email][:email_body]
       @subject = params[:send_email][:subject]
       Rails.env.development? || Rails.env.test? ? @email = 'expertiza.mailer@gmail.com' : @email = response.params[:email]
       mail(to: @email, body: @body, subject: @subject, content_type: 'text/html',)
   end

And sent in the Response model:

 # Send response email from reviewer to author
 def self.send_response_email
   ResponseMailer.with(response: self)
     .send_response_email
     .deliver_later
 end

Affected Classes

Changed existent classes

  • controllers/responses_controller.rb
  • models/responses.rb

New modules

  • response_helper.rb
  • response_mailer.rb

Pull Request

Here is our Pull Request: https://github.com/expertiza/reimplementation-back-end/pull/165.

Running the Project Locally

The project could be run locally by cloning the Github repository Expertiza Reimplementation and then running the following commands sequentially.

bundle install
rake db:create:all
rake db:migrate
rails s

Future Work