Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions app/controllers/response_maps_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# app/controllers/response_maps_controller.rb
# Handles CRUD operations and special actions for ResponseMaps
# ResponseMaps represent the relationship between a reviewer and reviewee
class ResponseMapsController < ApplicationController
before_action :set_response_map, only: [:show, :update, :destroy, :submit_response]

# Lists all response maps in the system
# GET /response_maps
def index
@response_maps = ResponseMap.all
render json: @response_maps
end

# Retrieves a specific response map by ID
# GET /response_maps/:id
def show
render json: @response_map
end

# Creates a new response map with the provided parameters
# POST /response_maps
def create
@response_map = ResponseMap.new(response_map_params)
persist_and_respond(@response_map, :created)
end

# Updates an existing response map with new attributes
# PATCH/PUT /response_maps/:id
def update
@response_map.assign_attributes(response_map_params)
persist_and_respond(@response_map, :ok)
end

# Removes a response map from the system
# DELETE /response_maps/:id
def destroy
@response_map.destroy
head :no_content
end

# Handles the submission of a response associated with a response map
# This also triggers email notifications if configured
# POST /response_maps/:id/submit_response
def submit_response
@response = @response_map.responses.find_or_initialize_by(id: params[:response_id])
@response.assign_attributes(response_params)
@response.is_submitted = true

if @response.save
# send feedback email now that it’s marked submitted
handle_submission(@response_map)
else
render json: { errors: @response.errors }, status: :unprocessable_entity
end
end

# Processes the actual submission and handles email notifications
# @param map [ResponseMap] The response map being submitted
def handle_submission(map)
FeedbackEmailMailer.new(map, map.assignment).call
render json: { message: 'Response submitted successfully, email sent' }, status: :ok
rescue StandardError => e
Rails.logger.error "FeedbackEmail failed: #{e.message}"
render json: { message: 'Response submitted, but email failed' }, status: :ok
end

# Generates a report of responses for a specific assignment
# Can be filtered by type and grouped by rounds if applicable
# GET /response_maps/response_report/:assignment_id
def response_report
assignment_id = params[:assignment_id]
report = ResponseMap.response_report(assignment_id, params[:type])
render json: report
end

# Retrieves all response maps for a specific assignment
# GET /response_maps/assignment/:assignment_id
def assignment_feedback
@response_maps = ResponseMap.for_assignment(params[:assignment_id])
render json: @response_maps
end

# Gets all response maps for a specific reviewer (includes responses)
# GET /response_maps/reviewer/:reviewer_id
def reviewer_feedback
@response_maps = ResponseMap.for_reviewer_with_responses(params[:reviewer_id])
render json: @response_maps, include: :responses
end

# Calculates and returns response statistics for an assignment
# GET /response_maps/response_rate/:assignment_id
def response_rate
stats = ResponseMap.response_rate_for_assignment(params[:assignment_id])
render json: stats
end

private

# Locates the response map by ID and sets it as an instance variable
# Renders a 404 error if the map is not found
def set_response_map
@response_map = ResponseMap.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Response map not found' }, status: :not_found
end

# Defines permitted parameters for response map creation/update
# @return [ActionController::Parameters] Whitelisted parameters
def response_map_params
params.require(:response_map).permit(:reviewee_id, :reviewer_id, :reviewed_object_id)
end

# Defines permitted parameters for response submission
# Includes nested attributes for scores
# @return [ActionController::Parameters] Whitelisted parameters
def response_params
params.require(:response).permit(
:additional_comment,
:round,
:is_submitted,
scores_attributes: [:answer, :comments, :question_id]
)
end

# Common method to persist records and generate appropriate responses
# Handles submission processing if the record is marked as submitted
# @param record [ActiveRecord::Base] The record to save
# @param success_status [Symbol] HTTP status code for successful save
def persist_and_respond(record, success_status)
if record.save
handle_submission(record) if record.is_submitted?
render json: record, status: success_status
else
render json: record.errors, status: :unprocessable_entity
end
end
end
29 changes: 29 additions & 0 deletions app/mailers/feedback_email_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class FeedbackEmailMailer < ApplicationMailer
# Initialize the mailer with a ResponseMap and an Assignment
def initialize(response_map, assignment)
@response_map = response_map
@assignment = assignment
end

# Public API method to trigger the email send
def call
Mailer.sync_message(build_defn).deliver
end

private

def build_defn
reviewee_id = Response.find(@response_map.reviewee_id)
participant = AssignmentParticipant.find(reviewee_id)
user = User.find(participant.user_id)

{
to: user.email,
body: {
type: 'Author Feedback',
first_name: user.fullname,
obj_name: @assignment.name
}
}
end
end
67 changes: 66 additions & 1 deletion app/models/feedback_response_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class FeedbackResponseMap < ResponseMap
belongs_to :review, class_name: 'Response', foreign_key: 'reviewed_object_id'
belongs_to :review, class_name: 'Response', foreign_key: 'reviewed_object_id'
belongs_to :reviewer, class_name: 'AssignmentParticipant', dependent: :destroy
belongs_to :reviewee, class_name: 'Participant', foreign_key: 'reviewee_id'

def assignment
review.map.assignment
Expand All @@ -20,4 +21,68 @@ def get_title
def questionnaire_type
'AuthorFeedback'
end
end

# Returns the original contributor (the author who received the review)
def contributor
self.reviewee
end

# Returns the reviewer who gave the original review
def reviewer
self.reviewer
end

# Returns the round number of the original review (if applicable)
def round
self&.response&.round
end

# Returns a report of feedback responses, grouped dynamically by round
def self.feedback_response_report(assignment_id, _type)
authors = fetch_authors_for_assignment(assignment_id)
review_map_ids = review_map_ids = ReviewResponseMap.where(["reviewed_object_id = ?", assignment_id]).pluck("id")
review_responses = Response.where(["map_id IN (?)", review_map_ids])
review_responses = review_responses.order("created_at DESC") if review_responses.respond_to?(:order)

if Assignment.find(assignment_id).varying_rubrics_by_round?
latest_by_map_and_round = {}

review_responses.each do |response|
key = [response.map_id, response.round]
latest_by_map_and_round[key] ||= response
end

grouped_by_round = latest_by_map_and_round.values.group_by(&:round)
sorted_by_round = grouped_by_round.sort.to_h # {round_number => [response1_id, response2_id, ...]}
response_ids_by_round = sorted_by_round.transform_values { |resps| resps.map(&:id) }

[authors] + response_ids_by_round.values
else
latest_by_map = {}

review_responses.each do |response|
latest_by_map[response.map_id] ||= response
end

[authors, latest_by_map.values.map(&:id)]
end
end

# Fetches all participants who authored submissions for the assignment
def self.fetch_authors_for_assignment(assignment_id)
Assignment.find(assignment_id).teams.includes(:users).flat_map do |team|
team.users.map do |user|
AssignmentParticipant.find_by(parent_id: assignment_id, user_id: user.id)
end
end.compact
end

def send_feedback_email(assignment)
FeedbackEmailMailer.new(self, assignment).call
end

# Build a new instance from controller params (keeps creation details centralized)
def self.build_from_params(params)
new(params)
end
end
50 changes: 50 additions & 0 deletions app/models/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ class Response < ApplicationRecord

alias map response_map
delegate :response_assignment, :reviewee, :reviewer, to: :map
# Delegate common methods to response_map for easier access
delegate :questionnaire, :reviewee, :reviewer, to: :map

validates :map_id, presence: true

# Callback to handle any post-submission actions
after_save :handle_response_submission

# Marks the response as submitted
# @return [Boolean] success of the submission update
def submit
update(is_submitted: true)
end

# Handles any necessary actions after a response is submitted
# Currently focuses on email notifications
# Only triggers when is_submitted changes from false to true
def handle_response_submission
return unless is_submitted_changed? && is_submitted?

# Send email notification through the response map
send_notification_email
end

# return the questionnaire that belongs to the response
def questionnaire
Expand Down Expand Up @@ -73,4 +96,31 @@ def maximum_score
# puts "total: #{total_weight * questionnaire.max_question_score} "
total_weight * questionnaire.max_question_score
end

# Sends notification emails when appropriate
# Currently handles feedback response notifications
def send_notification_email
return unless map.assignment.present?

if map.is_a?(FeedbackResponseMap)
FeedbackEmailMailer.new(map, map.assignment).call
end
# Add other response map type email services as needed
end

# Gets all active questions that can be scored
# @return [Array<Question>] list of active scored questions
def active_scored_questions
return [] if scores.empty?

questionnaire = questionnaire_by_answer(scores.first)
questionnaire.items.select(&:scorable?)
end

# Retrieves the questionnaire associated with an answer
# @param answer [Answer] the answer to find the questionnaire for
# @return [Questionnaire] the associated questionnaire
def questionnaire_by_answer(answer)
answer&.question&.questionnaire
end
end
Loading