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