diff --git a/app/controllers/course_reports_controller.rb b/app/controllers/course_reports_controller.rb new file mode 100644 index 000000000..93226bcf7 --- /dev/null +++ b/app/controllers/course_reports_controller.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +class CourseReportsController < ApplicationController + class FinalDueDateNotReviewDeadlineError < StandardError; end + + # only restruct for course staff (TA + instr) + def action_allowed? + case params[:action] + when 'index' + course = Course.find_by(id: params[:course_id]) + return current_user_has_instructor_privileges? || current_user_has_ta_privileges? if course.nil? + + current_user_teaching_staff_of_course?(course) + else + false + end + end + + # GET /course_reports + # Returns a table for all assignments in the given course, + # with students as rows and assignments as horizontal columns. + def index + course = Course.find_by(id: params[:course_id]) + return render json: { error: 'Course not found' }, status: :not_found unless course + + assignments = assignments_ordered_by(course, :final_review_due_date) + assignment_ids = assignments.map(&:id) + participants = AssignmentParticipant + .includes(:user, :assignment) + .where(parent_id: assignment_ids) + + student_rows = participants + .group_by(&:user_id) + .values + .map { |student_participants| build_student_row(assignments, student_participants) } + .sort_by { |row| row[:user_name].downcase } + + render json: course_report_response(course, assignments, student_rows), status: :ok + rescue FinalDueDateNotReviewDeadlineError => e + render json: { error: e.message }, status: :internal_server_error + end + + private + + def current_user_teaching_staff_of_course?(course) + user_logged_in? && ( + course.instructor_id == current_user.id || + TaMapping.exists?(user_id: current_user.id, course_id: course.id) + ) + end + + # metadata col + # indicates whether an assignment has optional columns eg topics. + def assignment_column(assignment) + { + assignment_id: assignment.id, + assignment_name: assignment.name, + has_topics: !!assignment.has_topics + } + end + + def assignments_ordered_by(course, field) + course.assignments.includes(:due_dates).sort_by do |assignment| + [assignment_sort_value(assignment, field), assignment.id] + end + end + + def assignment_sort_value(assignment, field) + case field + when :final_review_due_date + final_review_due_date_for(assignment) + else + assignment.public_send(field) + end + end + + # function to retrieve final review due date from the db: + # Raises FinalDueDateNotReviewDeadline as error + def final_review_due_date_for(assignment) + final_due_date = assignment.due_dates.max_by(&:due_at) + return final_due_date.due_at if final_due_date&.deadline_type_id == DueDate::REVIEW_DEADLINE_TYPE_ID + + # At this point of the project, all assignments are peer review assignments, + # so the final deadline is bound to be a review deadline, hence this guard + # + #Replace this with code in the incident that non peer review assignments are introduced + raise FinalDueDateNotReviewDeadlineError, + "Final due date for assignment #{assignment.id} is not a review deadline" + end + + # function to build full response, along with the metadata + student rows. + def course_report_response(course, assignments, student_rows) + { + course_id: course.id, + course_name: course.name, + assignments: assignments.map { |assignment| assignment_column(assignment) }, + students: student_rows + } + end + + # function to build each row of students (students x assignment matrix) + def build_student_row(assignments, student_participants) + first_participant = student_participants.first + participant_by_assignment = student_participants.index_by(&:parent_id) + + { + user_id: first_participant.user_id, + user_name: first_participant.user_name, + assignments: assignment_cells_for_student(assignments, participant_by_assignment) + } + end + + # per user assignment stats, combines assignment cells (next call )with student row + def assignment_cells_for_student(assignments, participant_by_assignment) + assignments.to_h do |assignment| + participant = participant_by_assignment[assignment.id] + [assignment.id.to_s, participant ? build_assignment_cell(assignment, participant) : nil] + end + end + + # building per-assignment cell. each cell corresponds to each assignment in a single student row. + def build_assignment_cell(assignment, participant) + team = participant.team + + { + participant_id: participant.id, + peer_grade: team&.aggregate_review_grade, + instructor_grade: team&.grade_for_submission, + avg_teammate_score: participant.aggregate_teammate_review_grade(teammate_review_maps_for(assignment, participant)), + avg_author_feedback_score: participant.aggregate_teammate_review_grade(author_feedback_maps_for(assignment, participant)) + }.tap do |cell| # optional topic col + cell[:topic] = topic_name_for(assignment, participant) if assignment.has_topics + end + end + + # get topic name if exists + def topic_name_for(assignment, participant) + return unless assignment.has_topics + + team_id = TeamsParticipant.find_by(participant_id: participant.id)&.team_id + return unless team_id + + SignedUpTeam.find_by(team_id: team_id)&.project_topic&.topic_name + end + + # response maps for teammate review. + + def teammate_review_maps_for(assignment, participant) + TeammateReviewResponseMap.where(reviewed_object_id: assignment.id, reviewee_id: participant.id) + end + + # response maps for auth feedback + def author_feedback_maps_for(assignment, participant) + review_maps = ReviewResponseMap.where(reviewed_object_id: assignment.id, reviewer_id: participant.id) + + FeedbackResponseMap.where(reviewed_object_id: review_maps.select(:id), reviewee_id: participant.id) + end +end diff --git a/config/routes.rb b/config/routes.rb index 57559d007..7a691dfb0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,21 +154,23 @@ end end - resources :participants do - collection do - get '/user/:user_id', to: 'participants#list_user_participants' - get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' - get '/:id', to: 'participants#show' + resources :participants do + collection do + get '/user/:user_id', to: 'participants#list_user_participants' + get '/assignment/:assignment_id', to: 'participants#list_assignment_participants' + get '/:id', to: 'participants#show' post '/:authorization', to: 'participants#add' patch '/:id/:authorization', to: 'participants#update_authorization' - delete '/:id', to: 'participants#destroy' - end - end - - resources :student_teams, only: %i[create update] do - collection do - get :view - get :mentor + delete '/:id', to: 'participants#destroy' + end + end + + resources :course_reports, only: [:index] + + resources :student_teams, only: %i[create update] do + collection do + get :view + get :mentor get :remove_participant put '/leave', to: 'student_teams#leave_team' end diff --git a/db/seeds.rb b/db/seeds.rb index d49c80f33..31d61b7e8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -57,23 +57,65 @@ puts "creating assignments" assignment_ids = [] + assignment_topics = {} + topic_assignment_indexes = (0...num_assignments).to_a.sample(num_assignments / 2) num_assignments.times do |i| - assignment_ids << Assignment.create( + has_topics = topic_assignment_indexes.include?(i) + assignment = Assignment.create( name: Faker::Verb.base, instructor_id: instructor_user_ids[i % num_instructors], course_id: course_ids[i % num_courses], has_teams: true, + has_topics: has_topics, private: false - ).id + ) + assignment_ids << assignment.id + + if has_topics + assignment_topics[assignment.id] = 2.times.map do |topic_index| + ProjectTopic.create!( + assignment_id: assignment.id, + topic_name: "Assignment #{i + 1} Topic #{topic_index + 1}", + topic_identifier: "A#{i + 1}T#{topic_index + 1}", + max_choosers: num_teams + ) + end + end + + AssignmentDueDate.create!( + parent: assignment, + due_at: (i * 3 + 1).days.from_now, + deadline_type_id: 1, + submission_allowed_id: DueDate::ALLOWED, + review_allowed_id: DueDate::ALLOWED + ) + + AssignmentDueDate.create!( + parent: assignment, + due_at: (i * 3 + 2).days.from_now, + deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID, + submission_allowed_id: DueDate::ALLOWED, + review_allowed_id: DueDate::ALLOWED + ) end puts "creating teams" team_ids = [] num_teams.times do |i| - team_ids << AssignmentTeam.create( + assignment_id = assignment_ids[i % num_assignments] + team = AssignmentTeam.create( name: "Team #{i + 1}", - parent_id: assignment_ids[i % num_assignments] - ).id + parent_id: assignment_id + ) + team_ids << team.id + + if assignment_topics[assignment_id].present? + SignedUpTeam.create!( + team_id: team.id, + project_topic_id: assignment_topics[assignment_id].sample.id, + is_waitlisted: false + ) + end end puts "creating students" @@ -138,3 +180,190 @@ rescue ActiveRecord::RecordInvalid => e puts e, 'The db has already been seeded' end + +puts 'creating course report data' + +course_report_course = Course.joins(:assignments).distinct.order(:id).first +raise('Seed at least one course with assignments before creating course report data') unless course_report_course + +course_report_assignments = course_report_course.assignments.order(:id).to_a +course_report_assignment_ids = course_report_assignments.map(&:id) + +course_report_user_ids = AssignmentParticipant + .where(parent_id: course_report_assignment_ids) + .where.not(user_id: nil) + .distinct + .pluck(:user_id) +course_report_users = User.where(id: course_report_user_ids).order(:id).to_a +raise('Seed at least one student participant in the selected course before creating course report data') if course_report_users.empty? + +course_report_instructor = course_report_course.instructor || User.joins(:role).where(roles: { name: 'Instructor' }).order(:id).first +raise('Seed at least one instructor before creating course report data') unless course_report_instructor + +def seed_course_report_participant(user, assignment) + participant = AssignmentParticipant.find_or_initialize_by(user_id: user.id, parent_id: assignment.id) + participant.handle ||= user.name + participant.save! + participant +end + +def seed_course_report_team(assignment, index) + team = AssignmentTeam.find_or_initialize_by( + name: "Course Report Team #{assignment.id}-#{index + 1}", + parent_id: assignment.id + ) + team.grade_for_submission = 80 + ((assignment.id + index) % 16) + team.save! + team +end + +def seed_course_report_members(team, participants) + participants.each do |participant| + team_member = TeamsParticipant.find_or_initialize_by(team_id: team.id, participant_id: participant.id) + team_member.user_id = participant.user_id + team_member.save! + end +end + +def seed_course_report_questionnaire(assignment, instructor) + questionnaire = Questionnaire.find_or_create_by!(name: "Course Report Rubric #{assignment.id}") do |rubric| + rubric.instructor_id = instructor.id + rubric.private = false + rubric.min_question_score = 0 + rubric.max_question_score = 5 + rubric.questionnaire_type = 'ReviewQuestionnaire' + rubric.display_type = 'Review' + end + + item = Criterion.find_or_create_by!( + questionnaire_id: questionnaire.id, + txt: "Course report score for assignment #{assignment.id}" + ) do |criterion| + criterion.weight = 1 + criterion.seq = 1 + criterion.question_type = 'Criterion' + criterion.size = '50,3' + criterion.break_before = true + end + + AssignmentQuestionnaire.find_or_create_by!( + assignment_id: assignment.id, + questionnaire_id: questionnaire.id, + used_in_round: nil + ) + + item +end + +def seed_course_report_response(map, item, answer_value, comments) + response = Response.find_or_initialize_by(map_id: map.id, round: nil) + response.is_submitted = true + response.save! + + answer = Answer.find_or_initialize_by(response_id: response.id, item_id: item.id) + answer.answer = answer_value + answer.comments = comments + answer.save! + + response +end + +def seed_course_report_review(assignment, reviewer, reviewee_team, item, score) + map = ReviewResponseMap.find_or_create_by!( + reviewed_object_id: assignment.id, + reviewer_id: reviewer.id, + reviewee_id: reviewee_team.id + ) + seed_course_report_response(map, item, score, "Seed review from #{reviewer.user_name} to #{reviewee_team.name}") + map +end + +def seed_course_report_teammate_review(assignment, reviewer, reviewee, item, score) + map = TeammateReviewResponseMap.find_or_create_by!( + reviewed_object_id: assignment.id, + reviewer_id: reviewer.id, + reviewee_id: reviewee.id + ) + seed_course_report_response(map, item, score, "Seed teammate review from #{reviewer.user_name} to #{reviewee.user_name}") + map +end + +def seed_course_report_author_feedback(review_map, reviewer, reviewee, item, score) + map = FeedbackResponseMap.find_or_create_by!( + reviewed_object_id: review_map.id, + reviewer_id: reviewer.id, + reviewee_id: reviewee.id + ) + seed_course_report_response(map, item, score, "Seed author feedback from #{reviewer.user_name} to #{reviewee.user_name}") +end + +course_report_assignments.each do |assignment| + participants = course_report_users.map { |user| seed_course_report_participant(user, assignment) } + item = seed_course_report_questionnaire(assignment, course_report_instructor) + + existing_team_groups = {} + participants_without_teams = [] + + participants.each do |participant| + team = participant.team + + if team + existing_team_groups[team.id] ||= [team, []] + existing_team_groups[team.id][1] << participant + else + participants_without_teams << participant + end + end + + existing_teams_with_members = existing_team_groups.values.each_with_index.map do |(team, team_participants), team_index| + team.update!(grade_for_submission: 80 + ((assignment.id + team_index) % 16)) + seed_course_report_members(team, team_participants) + + [team, team_participants] + end + + seeded_teams_with_members = participants_without_teams.each_slice(2).with_index.map do |team_participants, team_index| + team = seed_course_report_team(assignment, team_index) + seed_course_report_members(team, team_participants) + + [team, team_participants] + end + + teams_with_members = existing_teams_with_members + seeded_teams_with_members + + teams_with_members.each_with_index do |(team, _team_participants), team_index| + if assignment.has_topics + topics = ProjectTopic.where(assignment_id: assignment.id).order(:id).to_a + if topics.any? + existing_signup = SignedUpTeam + .joins(:project_topic) + .find_by(team_id: team.id, project_topics: { assignment_id: assignment.id }) + + SignedUpTeam.find_or_create_by!(team_id: team.id, project_topic_id: existing_signup&.project_topic_id || topics.sample.id) do |signup| + signup.is_waitlisted = false + end + end + end + end + + teams_with_members.each_with_index do |(team, team_participants), team_index| + reviewers = participants - team_participants + reviewers = participants if reviewers.empty? + + reviewers.first(2).each_with_index do |reviewer, reviewer_index| + review_score = 3 + ((assignment.id + team_index + reviewer_index) % 3) + review_map = seed_course_report_review(assignment, reviewer, team, item, review_score) + + feedback_reviewer = team_participants[reviewer_index % team_participants.size] + feedback_score = 3 + ((assignment.id + reviewer.id + team.id) % 3) + seed_course_report_author_feedback(review_map, feedback_reviewer, reviewer, item, feedback_score) + end + + team_participants.permutation(2).each_with_index do |(reviewer, reviewee), teammate_index| + teammate_score = 3 + ((assignment.id + reviewer.id + reviewee.id + teammate_index) % 3) + seed_course_report_teammate_review(assignment, reviewer, reviewee, item, teammate_score) + end + end +end + +puts "Course report seed course id: #{course_report_course.id}" diff --git a/spec/requests/api/v1/course_reports_controller_spec.rb b/spec/requests/api/v1/course_reports_controller_spec.rb new file mode 100644 index 000000000..d518c762e --- /dev/null +++ b/spec/requests/api/v1/course_reports_controller_spec.rb @@ -0,0 +1,501 @@ +require 'swagger_helper' +require 'json_web_token' +require 'json' + +RSpec.describe 'Course Reports API', type: :request do + before(:all) do + @roles = create_roles_hierarchy + end + + let(:instructor) do + User.create!( + name: 'instructor_records', + password_digest: 'password', + role_id: @roles[:instructor].id, + full_name: 'Instructor Records', + email: 'instructor_records@example.com' + ) + end + + let(:student) do + User.create!( + name: 'student_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Student Records', + email: 'student_records@example.com' + ) + end + + let(:teammate) do + User.create!( + name: 'teammate_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Teammate Records', + email: 'teammate_records@example.com' + ) + end + + let(:other_student) do + User.create!( + name: 'other_student_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Other Student Records', + email: 'other_student_records@example.com' + ) + end + + let(:reviewer_one) do + User.create!( + name: 'reviewer_one_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Reviewer One Records', + email: 'reviewer_one_records@example.com' + ) + end + + let(:reviewer_two) do + User.create!( + name: 'reviewer_two_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Reviewer Two Records', + email: 'reviewer_two_records@example.com' + ) + end + + let(:assignment2_partner) do + User.create!( + name: 'assignment2_partner_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Assignment2 Partner Records', + email: 'assignment2_partner_records@example.com' + ) + end + + let(:assignment2_reviewer) do + User.create!( + name: 'assignment2_reviewer_records', + password_digest: 'password', + role_id: @roles[:student].id, + full_name: 'Assignment2 Reviewer Records', + email: 'assignment2_reviewer_records@example.com' + ) + end + + let(:outside_instructor) do + User.create!( + name: 'outside_instructor_records', + password_digest: 'password', + role_id: @roles[:instructor].id, + full_name: 'Outside Instructor Records', + email: 'outside_instructor_records@example.com' + ) + end + + let!(:course) { create(:course, instructor: instructor) } + + let!(:assignment) do + Assignment.create!( + name: 'Assignment With Records', + instructor_id: instructor.id, + course_id: course.id, + has_topics: true + ) + end + + let!(:assignment2) do + Assignment.create!( + name: 'Assignment Without Topics', + instructor_id: instructor.id, + course_id: course.id, + has_topics: false + ) + end + + let!(:other_course) { create(:course) } + + let!(:assignment3) do + Assignment.create!( + name: 'Assignment Outside Course', + instructor_id: instructor.id, + course_id: other_course.id, + has_topics: false + ) + end + + let!(:team) { AssignmentTeam.create!(name: 'Records Team', parent_id: assignment.id, grade_for_submission: 91) } + let!(:team2) { AssignmentTeam.create!(name: 'Records Team 2', parent_id: assignment2.id, grade_for_submission: 84) } + let!(:team3) { AssignmentTeam.create!(name: 'Records Team 3', parent_id: assignment3.id, grade_for_submission: 73) } + let!(:review_team) { AssignmentTeam.create!(name: 'Review Team', parent_id: assignment.id, grade_for_submission: 88) } + let!(:assignment2_review_team) { AssignmentTeam.create!(name: 'Assignment 2 Review Team', parent_id: assignment2.id, grade_for_submission: 79) } + + let!(:participant) { AssignmentParticipant.create!(user_id: student.id, parent_id: assignment.id, handle: student.name) } + let!(:participant2) { AssignmentParticipant.create!(user_id: teammate.id, parent_id: assignment.id, handle: teammate.name) } + let!(:participant3) { AssignmentParticipant.create!(user_id: other_student.id, parent_id: assignment2.id, handle: other_student.name) } + let!(:participant4) { AssignmentParticipant.create!(user_id: other_student.id, parent_id: assignment3.id, handle: other_student.name) } + let!(:participant5) { AssignmentParticipant.create!(user_id: reviewer_one.id, parent_id: assignment.id, handle: reviewer_one.name) } + let!(:participant6) { AssignmentParticipant.create!(user_id: reviewer_two.id, parent_id: assignment.id, handle: reviewer_two.name) } + let!(:participant7) { AssignmentParticipant.create!(user_id: assignment2_partner.id, parent_id: assignment2.id, handle: assignment2_partner.name) } + let!(:participant8) { AssignmentParticipant.create!(user_id: assignment2_reviewer.id, parent_id: assignment2.id, handle: assignment2_reviewer.name) } + let!(:participant9) { AssignmentParticipant.create!(user_id: student.id, parent_id: assignment2.id, handle: student.name) } + let!(:participant10) { AssignmentParticipant.create!(user_id: teammate.id, parent_id: assignment2.id, handle: teammate.name) } + let!(:participant11) { AssignmentParticipant.create!(user_id: other_student.id, parent_id: assignment.id, handle: other_student.name) } + let!(:participant12) { AssignmentParticipant.create!(user_id: reviewer_one.id, parent_id: assignment2.id, handle: reviewer_one.name) } + let!(:participant13) { AssignmentParticipant.create!(user_id: reviewer_two.id, parent_id: assignment2.id, handle: reviewer_two.name) } + let!(:participant14) { AssignmentParticipant.create!(user_id: assignment2_partner.id, parent_id: assignment.id, handle: assignment2_partner.name) } + let!(:participant15) { AssignmentParticipant.create!(user_id: assignment2_reviewer.id, parent_id: assignment.id, handle: assignment2_reviewer.name) } + + let!(:topic) { ProjectTopic.create!(assignment_id: assignment.id, topic_name: 'Topic Alpha', topic_identifier: 'T1', max_choosers: 2) } + let!(:signed_up_team) { SignedUpTeam.create!(team_id: team.id, project_topic_id: topic.id, is_waitlisted: false) } + let!(:signed_up_review_team) { SignedUpTeam.create!(team_id: review_team.id, project_topic_id: topic.id, is_waitlisted: false) } + + let!(:questionnaire1) do + Questionnaire.create!( + name: 'Assignment 1 Records Questionnaire', + instructor_id: instructor.id, + private: false, + min_question_score: 0, + max_question_score: 5, + questionnaire_type: 'ReviewQuestionnaire' + ) + end + + let!(:questionnaire2) do + Questionnaire.create!( + name: 'Assignment 2 Records Questionnaire', + instructor_id: instructor.id, + private: false, + min_question_score: 0, + max_question_score: 5, + questionnaire_type: 'ReviewQuestionnaire' + ) + end + + let!(:criterion1) do + Criterion.create!( + questionnaire_id: questionnaire1.id, + txt: 'Quality of contribution', + weight: 1, + seq: 1, + question_type: 'Criterion', + size: '50,3', + break_before: true + ) + end + + let!(:criterion2) do + Criterion.create!( + questionnaire_id: questionnaire2.id, + txt: 'Quality of contribution', + weight: 1, + seq: 1, + question_type: 'Criterion', + size: '50,3', + break_before: true + ) + end + + let!(:assignment_questionnaire1) { AssignmentQuestionnaire.create!(assignment_id: assignment.id, questionnaire_id: questionnaire1.id) } + let!(:assignment_questionnaire2) { AssignmentQuestionnaire.create!(assignment_id: assignment2.id, questionnaire_id: questionnaire2.id) } + + let!(:assignment_review_due_date1) do + AssignmentDueDate.create!( + parent: assignment, + due_at: 12.days.from_now, + deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID, + submission_allowed_id: 3, + review_allowed_id: 3 + ) + end + + let!(:assignment_review_due_date2) do + AssignmentDueDate.create!( + parent: assignment, + due_at: 14.days.from_now, + deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID, + submission_allowed_id: 3, + review_allowed_id: 3 + ) + end + + let!(:assignment2_review_due_date) do + AssignmentDueDate.create!( + parent: assignment2, + due_at: 7.days.from_now, + deadline_type_id: DueDate::REVIEW_DEADLINE_TYPE_ID, + submission_allowed_id: 3, + review_allowed_id: 3 + ) + end + + let!(:review_map1) { ReviewResponseMap.create!(reviewed_object_id: assignment.id, reviewer_id: participant5.id, reviewee_id: team.id) } + let!(:review_map2) { ReviewResponseMap.create!(reviewed_object_id: assignment.id, reviewer_id: participant6.id, reviewee_id: team.id) } + let!(:review_map3) { ReviewResponseMap.create!(reviewed_object_id: assignment2.id, reviewer_id: participant8.id, reviewee_id: team2.id) } + let!(:review_map4) { ReviewResponseMap.create!(reviewed_object_id: assignment.id, reviewer_id: participant.id, reviewee_id: review_team.id) } + let!(:review_map5) { ReviewResponseMap.create!(reviewed_object_id: assignment.id, reviewer_id: participant2.id, reviewee_id: review_team.id) } + let!(:review_map6) { ReviewResponseMap.create!(reviewed_object_id: assignment2.id, reviewer_id: participant9.id, reviewee_id: assignment2_review_team.id) } + let!(:review_map7) { ReviewResponseMap.create!(reviewed_object_id: assignment2.id, reviewer_id: participant10.id, reviewee_id: assignment2_review_team.id) } + + let!(:review_response1) { Response.create!(map_id: review_map1.id, is_submitted: true) } + let!(:review_response2) { Response.create!(map_id: review_map2.id, is_submitted: true) } + let!(:review_response3) { Response.create!(map_id: review_map3.id, is_submitted: true) } + let!(:review_response4) { Response.create!(map_id: review_map4.id, is_submitted: true) } + let!(:review_response5) { Response.create!(map_id: review_map5.id, is_submitted: true) } + let!(:review_response6) { Response.create!(map_id: review_map6.id, is_submitted: true) } + let!(:review_response7) { Response.create!(map_id: review_map7.id, is_submitted: true) } + + let!(:review_answer1) { Answer.create!(response_id: review_response1.id, item_id: criterion1.id, answer: 4, comments: 'Strong work') } + let!(:review_answer2) { Answer.create!(response_id: review_response2.id, item_id: criterion1.id, answer: 5, comments: 'Excellent work') } + let!(:review_answer3) { Answer.create!(response_id: review_response3.id, item_id: criterion2.id, answer: 3, comments: 'Solid work') } + let!(:review_answer4) { Answer.create!(response_id: review_response4.id, item_id: criterion1.id, answer: 2, comments: 'Needs more polish') } + let!(:review_answer5) { Answer.create!(response_id: review_response5.id, item_id: criterion1.id, answer: 3, comments: 'Decent work') } + let!(:review_answer6) { Answer.create!(response_id: review_response6.id, item_id: criterion2.id, answer: 4, comments: 'Very good effort') } + let!(:review_answer7) { Answer.create!(response_id: review_response7.id, item_id: criterion2.id, answer: 5, comments: 'Excellent effort') } + + let!(:feedback_map1) { FeedbackResponseMap.create!(reviewed_object_id: review_map4.id, reviewer_id: participant11.id, reviewee_id: participant.id) } + let!(:feedback_map2) { FeedbackResponseMap.create!(reviewed_object_id: review_map5.id, reviewer_id: participant14.id, reviewee_id: participant2.id) } + let!(:feedback_map3) { FeedbackResponseMap.create!(reviewed_object_id: review_map6.id, reviewer_id: participant12.id, reviewee_id: participant9.id) } + let!(:feedback_map4) { FeedbackResponseMap.create!(reviewed_object_id: review_map7.id, reviewer_id: participant13.id, reviewee_id: participant10.id) } + + let!(:feedback_response1) { Response.create!(map_id: feedback_map1.id, is_submitted: true) } + let!(:feedback_response2) { Response.create!(map_id: feedback_map2.id, is_submitted: true) } + let!(:feedback_response3) { Response.create!(map_id: feedback_map3.id, is_submitted: true) } + let!(:feedback_response4) { Response.create!(map_id: feedback_map4.id, is_submitted: true) } + + let!(:feedback_answer1) { Answer.create!(response_id: feedback_response1.id, item_id: criterion1.id, answer: 4, comments: 'Helpful review') } + let!(:feedback_answer2) { Answer.create!(response_id: feedback_response2.id, item_id: criterion1.id, answer: 5, comments: 'Excellent review') } + let!(:feedback_answer3) { Answer.create!(response_id: feedback_response3.id, item_id: criterion2.id, answer: 3, comments: 'Useful review') } + let!(:feedback_answer4) { Answer.create!(response_id: feedback_response4.id, item_id: criterion2.id, answer: 2, comments: 'Needs better review detail') } + + let!(:teammate_review_map1) { TeammateReviewResponseMap.create!(reviewed_object_id: assignment.id, reviewer_id: participant2.id, reviewee_id: participant.id) } + let!(:teammate_review_map2) { TeammateReviewResponseMap.create!(reviewed_object_id: assignment.id, reviewer_id: participant.id, reviewee_id: participant2.id) } + let!(:teammate_review_map3) { TeammateReviewResponseMap.create!(reviewed_object_id: assignment2.id, reviewer_id: participant7.id, reviewee_id: participant3.id) } + let!(:teammate_review_map4) { TeammateReviewResponseMap.create!(reviewed_object_id: assignment2.id, reviewer_id: participant3.id, reviewee_id: participant7.id) } + + let!(:teammate_response1) { Response.create!(map_id: teammate_review_map1.id, is_submitted: true) } + let!(:teammate_response2) { Response.create!(map_id: teammate_review_map2.id, is_submitted: true) } + let!(:teammate_response3) { Response.create!(map_id: teammate_review_map3.id, is_submitted: true) } + let!(:teammate_response4) { Response.create!(map_id: teammate_review_map4.id, is_submitted: true) } + + let!(:teammate_answer1) { Answer.create!(response_id: teammate_response1.id, item_id: criterion1.id, answer: 3, comments: 'Good teammate') } + let!(:teammate_answer2) { Answer.create!(response_id: teammate_response2.id, item_id: criterion1.id, answer: 4, comments: 'Reliable teammate') } + let!(:teammate_answer3) { Answer.create!(response_id: teammate_response3.id, item_id: criterion2.id, answer: 2, comments: 'Needs improvement') } + let!(:teammate_answer4) { Answer.create!(response_id: teammate_response4.id, item_id: criterion2.id, answer: 5, comments: 'Outstanding partner') } + + let(:instructor_token) { JsonWebToken.encode({ id: instructor.id }) } + let(:student_token) { JsonWebToken.encode({ id: student.id }) } + let(:outside_instructor_token) { JsonWebToken.encode({ id: outside_instructor.id }) } + let(:Authorization) { "Bearer #{instructor_token}" } + + before do + team.add_member(participant) + team.add_member(participant2) + review_team.add_member(participant11) + review_team.add_member(participant14) + review_team.add_member(participant15) + team2.add_member(participant3) + team2.add_member(participant7) + team2.add_member(participant9) + team2.add_member(participant10) + team3.add_member(participant4) + review_team.add_member(participant5) + review_team.add_member(participant6) + assignment2_review_team.add_member(participant8) + assignment2_review_team.add_member(participant12) + assignment2_review_team.add_member(participant13) + end + + path '/course_reports' do + get 'Retrieve a student-by-assignment table for a course' do + tags 'Course Reports' + produces 'application/json' + security [bearer_auth: []] + + parameter name: :course_id, in: :query, type: :integer, required: true, description: 'ID of the course' + parameter name: 'Authorization', in: :header, type: :string, required: true, description: 'Bearer token' + + response '200', 'Returns a student-by-assignment table for the course' do + let(:course_id) { course.id } + + run_test! do |response| + data = JSON.parse(response.body) + puts "\nCourse report stats:\n#{JSON.pretty_generate(data)}" + + expect(data['course_id']).to eq(course.id) + expect(data['course_name']).to eq(course.name) + expect(data.keys).to match_array(%w[course_id course_name assignments students]) + expect(data['assignments'].length).to eq(2) + expect(data['assignments'].first.keys).to match_array(%w[assignment_id assignment_name has_topics]) + assignment_ids_ordered_by_final_review_deadline = [assignment, assignment2] + .sort_by do |assignment_record| + assignment_record.due_dates + .select { |due_date| due_date.deadline_type_id == DueDate::REVIEW_DEADLINE_TYPE_ID } + .map(&:due_at) + .max + end + .map(&:id) + + expect(data['assignments'].map { |item| item['assignment_id'] }).to eq( + assignment_ids_ordered_by_final_review_deadline + ) + expect(assignment_review_due_date2.due_at).to be > assignment2_review_due_date.due_at + expect(data['assignments'].last['assignment_id']).to eq(assignment.id) + expect(data['assignments'].map { |item| item['assignment_name'] }).to match_array(['Assignment With Records', 'Assignment Without Topics']) + expect(data['assignments'].find { |item| item['assignment_id'] == assignment.id }['has_topics']).to eq(true) + expect(data['assignments'].find { |item| item['assignment_id'] == assignment2.id }['has_topics']).to eq(false) + expect(data['students'].length).to eq(7) + + student_row = data['students'].find { |item| item['user_id'] == student.id } + expect(student_row.keys).to match_array(%w[user_id user_name assignments]) + expect(student_row['user_name']).to eq(student.name) + expect(student_row['assignments'].keys).to match_array([assignment.id.to_s, assignment2.id.to_s]) + expect(student_row['assignments'][assignment.id.to_s].keys).to match_array( + %w[participant_id peer_grade instructor_grade avg_teammate_score avg_author_feedback_score topic] + ) + expect(student_row['assignments'][assignment2.id.to_s].keys).to match_array( + %w[participant_id peer_grade instructor_grade avg_teammate_score avg_author_feedback_score] + ) + expect(student_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant.id) + expect(student_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(90.0) + expect(student_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(91) + expect(student_row['assignments'][assignment.id.to_s]['avg_teammate_score']).to eq(60.0) + expect(student_row['assignments'][assignment.id.to_s]['avg_author_feedback_score']).to eq(80.0) + expect(student_row['assignments'][assignment.id.to_s]['topic']).to eq('Topic Alpha') + expect(student_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant9.id) + expect(student_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(60.0) + expect(student_row['assignments'][assignment2.id.to_s]['instructor_grade']).to eq(84) + expect(student_row['assignments'][assignment2.id.to_s]['avg_author_feedback_score']).to eq(60.0) + + teammate_row = data['students'].find { |item| item['user_id'] == teammate.id } + expect(teammate_row['user_name']).to eq(teammate.name) + expect(teammate_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant2.id) + expect(teammate_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(90.0) + expect(teammate_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(91) + expect(teammate_row['assignments'][assignment.id.to_s]['avg_teammate_score']).to eq(80.0) + expect(teammate_row['assignments'][assignment.id.to_s]['avg_author_feedback_score']).to eq(100.0) + expect(teammate_row['assignments'][assignment.id.to_s]['topic']).to eq('Topic Alpha') + expect(teammate_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant10.id) + expect(teammate_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(60.0) + expect(teammate_row['assignments'][assignment2.id.to_s]['instructor_grade']).to eq(84) + expect(teammate_row['assignments'][assignment2.id.to_s]['avg_author_feedback_score']).to eq(40.0) + + other_student_row = data['students'].find { |item| item['user_id'] == other_student.id } + expect(other_student_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant11.id) + expect(other_student_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(50.0) + expect(other_student_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(88) + expect(other_student_row['assignments'][assignment.id.to_s]['avg_author_feedback_score']).to be_nil + expect(other_student_row['assignments'][assignment.id.to_s]['topic']).to eq('Topic Alpha') + expect(other_student_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant3.id) + expect(other_student_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(60.0) + expect(other_student_row['assignments'][assignment2.id.to_s]['instructor_grade']).to eq(84) + expect(other_student_row['assignments'][assignment2.id.to_s]['avg_teammate_score']).to eq(40.0) + expect(other_student_row['assignments'][assignment2.id.to_s]).not_to have_key('topic') + + reviewer_row = data['students'].find { |item| item['user_id'] == reviewer_one.id } + expect(reviewer_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant5.id) + expect(reviewer_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(50.0) + expect(reviewer_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(88) + expect(reviewer_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant12.id) + expect(reviewer_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(90.0) + expect(reviewer_row['assignments'][assignment2.id.to_s]['instructor_grade']).to eq(79) + + reviewer_two_row = data['students'].find { |item| item['user_id'] == reviewer_two.id } + expect(reviewer_two_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant6.id) + expect(reviewer_two_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(88) + expect(reviewer_two_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(50.0) + expect(reviewer_two_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant13.id) + expect(reviewer_two_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(90.0) + expect(reviewer_two_row['assignments'][assignment2.id.to_s]['instructor_grade']).to eq(79) + + assignment2_partner_row = data['students'].find { |item| item['user_id'] == assignment2_partner.id } + expect(assignment2_partner_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant14.id) + expect(assignment2_partner_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(50.0) + expect(assignment2_partner_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(88) + expect(assignment2_partner_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant7.id) + expect(assignment2_partner_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(60.0) + expect(assignment2_partner_row['assignments'][assignment2.id.to_s]['avg_teammate_score']).to eq(100.0) + + assignment2_reviewer_row = data['students'].find { |item| item['user_id'] == assignment2_reviewer.id } + expect(assignment2_reviewer_row['assignments'][assignment.id.to_s]['participant_id']).to eq(participant15.id) + expect(assignment2_reviewer_row['assignments'][assignment.id.to_s]['peer_grade']).to eq(50.0) + expect(assignment2_reviewer_row['assignments'][assignment.id.to_s]['instructor_grade']).to eq(88) + expect(assignment2_reviewer_row['assignments'][assignment2.id.to_s]['participant_id']).to eq(participant8.id) + expect(assignment2_reviewer_row['assignments'][assignment2.id.to_s]['peer_grade']).to eq(90.0) + expect(assignment2_reviewer_row['assignments'][assignment2.id.to_s]['instructor_grade']).to eq(79) + + outside_course_ids = data['assignments'].map { |item| item['assignment_id'] } + expect(outside_course_ids).not_to include(assignment3.id) + end + end + + response '404', 'Course not found' do + let(:course_id) { 999_999 } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eq('Course not found') + end + end + + response '500', 'Final assignment due date is not a review deadline' do + let(:course_id) { course.id } + + let!(:assignment_submission_due_date) do + AssignmentDueDate.create!( + parent: assignment2, + due_at: 21.days.from_now, + deadline_type_id: 1, + submission_allowed_id: 3, + review_allowed_id: 3 + ) + end + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eq( + "Final due date for assignment #{assignment2.id} is not a review deadline" + ) + end + end + + response '403', 'Forbidden for students' do + let(:course_id) { course.id } + let(:Authorization) { "Bearer #{student_token}" } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eq('You are not authorized to index this course_reports') + end + end + + response '403', 'Forbidden for instructors outside the course teaching staff' do + let(:course_id) { course.id } + let(:Authorization) { "Bearer #{outside_instructor_token}" } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eq('You are not authorized to index this course_reports') + end + end + + response '401', 'Unauthorized' do + let(:course_id) { course.id } + let(:Authorization) { 'Bearer invalid_token' } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eq('Not Authorized') + end + end + + response '401', 'Unauthorized without a bearer token' do + let(:course_id) { course.id } + let(:Authorization) { '' } + + run_test! do |response| + expect(JSON.parse(response.body)['error']).to eq('Not Authorized') + end + end + end + end +end