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
1 change: 1 addition & 0 deletions app/controllers/concerns/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def current_user_teaching_staff_of_assignment?(assignment_id)
(
current_user_instructs_assignment?(assignment) ||
current_user_has_ta_mapping_for_assignment?(assignment)
# TODO: include a check to allow admins or superadmins access to assignments their child instructors can access
)
end

Expand Down
101 changes: 100 additions & 1 deletion app/controllers/grades_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def action_allowed?
when 'view_our_scores','view_my_scores'
set_participant_and_team_via_assignment
current_user_is_assignment_participant?(params[:assignment_id])
when 'view_all_scores'
when 'view_all_scores', 'get_review_tableau_data'
current_user_teaching_staff_of_assignment?(params[:assignment_id])
when 'edit', 'assign_grade', 'instructor_review'
set_team_and_assignment_via_participant
Expand Down Expand Up @@ -54,6 +54,105 @@ def view_my_scores
end


# (GET /api/v1/grades/:assignment_id/:participant_id/get_review_tableau_data)
# Given an AssignmentParticipant ID, gather and return all reviews completed by that participant for the corresponding assignment.
def get_review_tableau_data
responses_by_round = {}
begin
# Determine all questionnaires used as part of this assignment, grouped by the round in which they are used.
AssignmentQuestionnaire.where("assignment_id = " + params[:assignment_id]).find_each do |pairing|
round_id = pairing[:used_in_round]
rubric_id = pairing[:questionnaire_id]

# If this round has not been recorded yet, record it.
if !responses_by_round.key?(round_id)
responses_by_round[round_id] = {}
end
# If this questionnaire has not been recorded yet, record it.
if !responses_by_round[round_id].key?(rubric_id)
# Items (the "questions") are always the same across responses of the same rubric.
# Initialize them into a hash using a helper function.
responses_by_round[round_id] = get_items_from_questionnaire(rubric_id)
end
end

response_mapping_condition = "reviewed_object_id = " + params[:assignment_id] + " AND reviewer_id = " + params[:participant_id]
ReviewResponseMap.where(response_mapping_condition).find_each do |mapping|
Response.where("map_id = " + mapping[:id].to_s).find_each do |response|

# response = Response.find_by(map_id: mapping[:id])

if response == nil
# If, for some reason, there is no response with this mapping id, move on to the next mapping id.
next
end

response_id = response[:id]
round_id = response[:round]

if !responses_by_round.key?(round_id)
# If, for some reason, there is no questionnaire associated with the given round, move on to the next mapping id.
next
end

# Record this response's values and comments, one pair for each item in the corresponding questionnaire.
responses_by_round[round_id].each_key do |item_id|
response_answer = Answer.find_by(item_id: item_id, response_id: response_id)
responses_by_round[round_id][item_id][:answers][:values].append(response_answer[:answer])
responses_by_round[round_id][item_id][:answers][:comments].append(response_answer[:comments])
end
end
end

# Get participant and user information for the response
participant = AssignmentParticipant.find(params[:participant_id])
assignment = Assignment.find(params[:assignment_id])

# Return JSON containing all answer values and comments associated with this reviewer and for this assignment.
render json: {
responses_by_round: responses_by_round,
participant: {
id: participant.id,
user_id: participant.user_id,
user_name: participant.user.name,
full_name: participant.user.full_name,
handle: participant.handle
},
assignment: {
id: assignment.id,
name: assignment.name
}
}
rescue ActiveRecord::RecordNotFound
render json: { error: "Participant or assignment not found" }, status: :not_found
rescue StandardError => e
render json: { error: "Internal server error" }, status: :internal_server_error
end
end

# A helper function which, given a questionnaire id, returns a hash keyed by the ids of that questionnaire's items.
# The values of the hash include the description (usually a question) of the item, and an empty hash for including responses.
def get_items_from_questionnaire(questionnaire_id)
questionnaire = Questionnaire.find_by(id: questionnaire_id)
item_data = {
min_answer_value: questionnaire[:min_question_score],
max_answer_value: questionnaire[:max_question_score],
items: {}
}
Item.where("questionnaire_id = " + questionnaire_id.to_s).find_each do |item|
item_data[:items][item[:id]] = {
description: item[:txt],
question_type: item[:question_type],
answers: {
values: [],
comments: []
}
}
end
return item_data
end


# edit (GET /api/v1/grades/:participant_id/edit)
# provides data for the grade-assignment interface.
# Given an AssignmentParticipant ID, it looks up the participant and its assignment, gathers the full list of items
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
get '/:assignment_id/view_all_scores', to: 'grades#view_all_scores'
patch '/:participant_id/assign_grade', to: 'grades#assign_grade'
get '/:participant_id/edit', to: 'grades#edit'
get '/:assignment_id/:participant_id/get_review_tableau_data', to: 'grades#get_review_tableau_data'
get '/:assignment_id/view_our_scores', to: 'grades#view_our_scores'
get '/:assignment_id/view_my_scores', to: 'grades#view_my_scores'
get '/:participant_id/instructor_review', to: 'grades#instructor_review'
Expand Down
176 changes: 176 additions & 0 deletions spec/controllers/grades_controller_get_review_tableau_data_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
require 'rails_helper'

RSpec.describe GradesController, type: :controller do
let(:assignment_id) { "10" }
let(:participant_id) { "20" }

# Fake User
let(:user) { instance_double(User, id: 99, name: "ReviewerUser", full_name: "Reviewer User") }

# Fake AssignmentParticipant
let(:fake_participant) do
double("AssignmentParticipant", id: participant_id.to_i, user_id: user.id, user: user, handle: "reviewer_handle").tap do |p|
allow(p).to receive(:[]).with(:id).and_return(p.id)
allow(p).to receive(:[]).with(:user_id).and_return(p.user_id)
allow(p).to receive(:[]).with(:handle).and_return(p.handle)
end
end

# Fake Assignment
let(:assignment) { instance_double(Assignment, id: assignment_id.to_i, name: "Test Assignment") }

# Fake AssignmentQuestionnaire
let(:fake_questionnaire) do
double("AssignmentQuestionnaire", used_in_round: 1, questionnaire_id: 999).tap do |q|
allow(q).to receive(:[]).with(:used_in_round).and_return(1)
allow(q).to receive(:[]).with(:questionnaire_id).and_return(999)
end
end

# Fake Item
let(:fake_item) do
double("Item", id: 1, txt: "Criterion 1", question_type: "Scale").tap do |item|
allow(item).to receive(:[]).with(:id).and_return(item.id)
allow(item).to receive(:[]).with(:txt).and_return(item.txt)
allow(item).to receive(:[]).with(:question_type).and_return(item.question_type)
end
end

# Fake ReviewResponseMap
let(:fake_response_map) { instance_double(ReviewResponseMap, id: 555) }

# Fake Response
let(:fake_response) { instance_double(Response, id: 777, round: 1) }

# Fake Answer
let(:fake_answer) { instance_double(Answer, answer: 4, comments: "Good work") }

# Expected JSON keys
let(:expected_json_keys) { %w[responses_by_round participant assignment] }

before do
# Stub ActiveRecord finders
allow(Assignment).to receive(:find).with(assignment_id).and_return(assignment)
allow(AssignmentParticipant).to receive(:find).with(participant_id).and_return(fake_participant)

# Stub AssignmentQuestionnaire query
fake_relation = double("ActiveRecord::Relation")
allow(fake_relation).to receive(:find_each).and_yield(fake_questionnaire)
allow(AssignmentQuestionnaire).to receive(:where)
.with("assignment_id = #{assignment_id}")
.and_return(fake_relation)

# Stub Item query
fake_item_relation = double("ActiveRecord::Relation")
allow(fake_item_relation).to receive(:find_each).and_yield(fake_item)
allow(Item).to receive(:where).with("questionnaire_id = 999").and_return(fake_item_relation)

# Use simple hashes instead of instance_double for hash-style access
fake_response_map = { id: 555 }
fake_response = { id: 777, round: 1 }
fake_item = { id: 1, txt: "Criterion 1", question_type: "Scale" }
fake_answer = { answer: 4, comments: "Good work" }

# Stub ReviewResponseMap.where(...).find_each
fake_review_map_relation = double("ActiveRecord::Relation")
allow(fake_review_map_relation).to receive(:find_each).and_yield(fake_response_map)
allow(ReviewResponseMap).to receive(:where)
.with("reviewed_object_id = #{assignment_id} AND reviewer_id = #{participant_id}")
.and_return(fake_review_map_relation)

# Stub Response.where(...).find_each
fake_response_relation = double("ActiveRecord::Relation")
allow(fake_response_relation).to receive(:find_each).and_yield(fake_response)
allow(Response).to receive(:where)
.with("map_id = #{fake_response_map[:id]}")
.and_return(fake_response_relation)

# Stub Answer.find_by(...)
allow(Answer).to receive(:find_by)
.with(item_id: fake_item[:id], response_id: fake_response[:id])
.and_return(fake_answer)

# Controller authorization stubs
allow(controller).to receive(:authorize).and_return(true)
allow(controller).to receive(:current_user).and_return(user)
allow(controller).to receive(:has_role?).and_return(true)
allow(controller).to receive(:action_allowed?).and_return(true)

# Stub JWT decoding (if using token auth)
request.headers['Authorization'] = 'Bearer faketoken'
allow(JsonWebToken).to receive(:decode).and_return({ id: user.id })
allow(User).to receive(:find).with(user.id).and_return(user)
end

describe "GET #get_review_tableau_data" do
it "responds with valid JSON containing required keys" do
get :get_review_tableau_data, params: { assignment_id: assignment_id, participant_id: participant_id }

expect(response).to have_http_status(:ok)

json = JSON.parse(response.body)
expect(json.keys).to include(*expected_json_keys)
end

it "returns 404 if participant is not found" do
allow(AssignmentParticipant).to receive(:find).with(participant_id).and_raise(ActiveRecord::RecordNotFound)

get :get_review_tableau_data, params: { assignment_id: assignment_id, participant_id: participant_id }

expect(response).to have_http_status(:not_found)
json = JSON.parse(response.body)
expect(json["error"]).to match(/Participant or assignment not found/)
end

it "returns 404 if assignment is not found" do
allow(Assignment).to receive(:find).with(assignment_id).and_raise(ActiveRecord::RecordNotFound)

get :get_review_tableau_data, params: { assignment_id: assignment_id, participant_id: participant_id }

expect(response).to have_http_status(:not_found)
json = JSON.parse(response.body)
expect(json["error"]).to match(/Participant or assignment not found/)
end

it "returns participant info correctly" do
get :get_review_tableau_data, params: { assignment_id: assignment_id, participant_id: participant_id }

json = JSON.parse(response.body)
participant_json = json["participant"]
expect(participant_json["id"]).to eq(fake_participant.id)
expect(participant_json["user_id"]).to eq(fake_participant.user_id)
expect(participant_json["user_name"]).to eq(fake_participant.user.name)
expect(participant_json["full_name"]).to eq(fake_participant.user.full_name)
expect(participant_json["handle"]).to eq(fake_participant.handle)
end

it "returns assignment info correctly" do
get :get_review_tableau_data, params: { assignment_id: assignment_id, participant_id: participant_id }

json = JSON.parse(response.body)
assignment_json = json["assignment"]
expect(assignment_json["id"]).to eq(assignment.id)
expect(assignment_json["name"]).to eq(assignment.name)
end

context "when user is a student (forbidden)" do
let(:student_user) { instance_double(User, id: 50, name: "StudentUser", full_name: "Student User", role_id: 1) }

before do
allow(controller).to receive(:current_user).and_return(student_user)
# allow(controller).to receive(:action_allowed?).and_return(false) # unauthorized
allow(controller).to receive(:authorize).and_call_original
allow(controller).to receive(:all_actions_allowed?).and_return(false)
end

it "returns 403 Unauthorized" do
get :get_review_tableau_data, params: { assignment_id: assignment_id, participant_id: participant_id }

expect(response).to have_http_status(:forbidden)

json = JSON.parse(response.body)
expect(json["error"]).to match(/You are not authorized to get_review_tableau_data this grades/)
end
end
end
end
7 changes: 6 additions & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
# config.fixture_path = Rails.root.join('spec/fixtures')

if config.respond_to?(:fixture_paths=)
config.fixture_paths = [Rails.root.join('spec/fixtures').to_s]
else
# fallback for older Rails / rspec-rails
config.fixture_path = Rails.root.join('spec/fixtures')
end
# Since we're using Factory Bot instead of fixtures, we don't need fixture_path
# config.fixture_path is deprecated in newer RSpec versions anyway

Expand Down