CSC/ECE 517 Spring 2025 - E2512. Reimplement responses controller
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