diff --git a/app/controllers/review_reports_controller.rb b/app/controllers/review_reports_controller.rb new file mode 100644 index 000000000..cde99012e --- /dev/null +++ b/app/controllers/review_reports_controller.rb @@ -0,0 +1,98 @@ +class ReviewReportsController < ApplicationController + # GET /review_reports/:assignment_id + def index + assignment = Assignment.find(params[:assignment_id]) + # Fetch all review response maps for this assignment + review_maps = ReviewResponseMap.where(reviewed_object_id: assignment.id) + + report_data = review_maps.map do |map| + reviewer = map.reviewer.user + reviewee = map.reviewee # Team + + # Get all responses + responses = Response.where(map_id: map.id).order(created_at: :asc) + + rounds = responses.map do |response| + questionnaire = response.questionnaire_by_answer(response.scores.first) + assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id) + round_num = assignment_questionnaire&.used_in_round || 1 # Default to 1 if not specified + + { + round: round_num, + calculatedScore: response.aggregate_questionnaire_score, + maxScore: response.maximum_score, + reviewVolume: response.volume, + reviewCommentCount: response.comment_count + } + end + + # Use the latest response for general status, but keep rounds data + latest_response = responses.last + + # Calculate reviews done/selected + reviews_selected = 1 + reviews_completed = latest_response&.is_submitted ? 1 : 0 + + # Calculate score (latest) + score = latest_response&.aggregate_questionnaire_score + max_score = latest_response&.maximum_score + + # Calculate volume (latest) + vol = latest_response&.volume || 0 + + # Determine status color + status = if !latest_response + "purple" # No review + elsif !latest_response.is_submitted + "red" # Not completed + elsif latest_response.is_submitted && map.reviewer_grade.nil? + "blue" # Completed, no grade + elsif map.reviewer_grade + "brown" # Grade assigned + else + "green" # Fallback or specific case (No submitted work?) + end + + { + id: map.id, + reviewerName: reviewer.full_name, + reviewerUsername: reviewer.name, + reviewerId: reviewer.id, + reviewsCompleted: reviews_completed, + reviewsSelected: reviews_selected, + teamReviewedName: reviewee.name, + hasConsent: map.reviewer.permission_granted, + teamReviewedStatus: status, + calculatedScore: score, # Latest score + maxScore: max_score, # Latest max score + rounds: rounds, # All rounds data + reviewComment: latest_response&.additional_comment, + reviewVolume: vol, + reviewCommentCount: latest_response&.comment_count || 0, + assignedGrade: map.reviewer_grade, + instructorComment: map.reviewer_comment + } + end + + # Calculate average volume for the assignment + total_volume = report_data.sum { |d| d[:reviewVolume] } + count_volume = report_data.count { |d| d[:reviewVolume] > 0 } + avg_volume = count_volume > 0 ? total_volume.to_f / count_volume : 0 + + render json: { + reportData: report_data, + averageVolume: avg_volume + } + end + + # PATCH /review_reports/:id/update_grade + # Updates the grade and comment for a specific review report + def update_grade + map = ReviewResponseMap.find(params[:id]) + if map.update(reviewer_grade: params[:assignedGrade], reviewer_comment: params[:instructorComment]) + render json: { message: "Grade updated successfully" }, status: :ok + else + render json: { error: "Failed to update grade" }, status: :unprocessable_entity + end + end +end diff --git a/app/models/Item.rb b/app/models/Item.rb index 7d8cf2ed5..0ba6ab259 100644 --- a/app/models/Item.rb +++ b/app/models/Item.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Item < ApplicationRecord + self.inheritance_column = 'question_type' + before_create :set_seq belongs_to :questionnaire # each item belongs to a specific questionnaire has_many :answers, dependent: :destroy, foreign_key: 'item_id' @@ -73,4 +75,9 @@ def self.for(record) # Cast the existing record to the desired subclass klass.new(record.attributes) end + + def self.find_with_order(ids) + return [] if ids.empty? + where(id: ids).index_by(&:id).slice(*ids).values + end end \ No newline at end of file diff --git a/app/models/answer.rb b/app/models/answer.rb index 3c3320afe..f5f16c70f 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -2,5 +2,5 @@ class Answer < ApplicationRecord belongs_to :response - belongs_to :item + belongs_to :item, foreign_key: 'question_id' end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 45ac849fb..52afbca3d 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -20,7 +20,8 @@ class Assignment < ApplicationRecord attr_accessor :title, :description, :has_badge, :enable_pair_programming, :is_calibrated, :staggered_deadline def review_questionnaire_id - Questionnaire.find_by_assignment_id id + aq = AssignmentQuestionnaire.find_by(assignment_id: id) + aq ? aq.questionnaire_id : nil end def teams? diff --git a/app/models/response.rb b/app/models/response.rb index 1dd9ba045..76e571ce5 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -73,4 +73,20 @@ def maximum_score # puts "total: #{total_weight * questionnaire.max_question_score} " total_weight * questionnaire.max_question_score end -end \ No newline at end of file + + def volume + text = (additional_comment || "") + scores.each do |s| + text += " " + (s.comments || "") + end + text.downcase.scan(/\b\w+\b/).uniq.count + end + + def comment_count + count = 0 + scores.each do |s| + count += 1 if s.comments.present? + end + count + end +end diff --git a/config/routes.rb b/config/routes.rb index 25642363c..e4a31c952 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -149,4 +149,12 @@ get '/:participant_id/instructor_review', to: 'grades#instructor_review' end end -end \ No newline at end of file + resources :review_reports, only: [] do + collection do + get ':assignment_id', action: :index + end + member do + patch 'update_grade', action: :update_grade + end + end +end diff --git a/db/migrate/20251203001749_add_grade_and_comment_to_response_maps.rb b/db/migrate/20251203001749_add_grade_and_comment_to_response_maps.rb new file mode 100644 index 000000000..aae84baaf --- /dev/null +++ b/db/migrate/20251203001749_add_grade_and_comment_to_response_maps.rb @@ -0,0 +1,6 @@ +class AddGradeAndCommentToResponseMaps < ActiveRecord::Migration[8.0] + def change + add_column :response_maps, :reviewer_grade, :integer + add_column :response_maps, :reviewer_comment, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index d3a15fcfa..f89151011 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_10_29_071649) do +ActiveRecord::Schema[8.0].define(version: 2025_12_03_001749) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -296,6 +296,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "type" + t.integer "reviewer_grade" + t.text "reviewer_comment" t.index ["reviewer_id"], name: "fk_response_map_reviewer" end diff --git a/db/seeds.rb b/db/seeds.rb index 8c11894f0..19671395a 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,128 +1,300 @@ # frozen_string_literal: true +# FactoryBot is required for the new E2562 seeding logic, but we use direct model creation instead of FactoryBot.create +require 'factory_bot_rails' + begin - # Create an instritution - inst_id = Institution.create!( - name: 'North Carolina State University' - ).id - - Role.create!(id: 1, name: 'Super Administrator') - Role.create!(id: 2, name: 'Administrator') - Role.create!(id: 3, name: 'Instructor') - Role.create!(id: 4, name: 'Teaching Assistant') - Role.create!(id: 5, name: 'Student') - - # Create an admin user - User.create!( - name: 'admin', - email: 'admin2@example.com', - password: 'password123', - full_name: 'admin admin', - institution_id: 1, - role_id: 1 - ) - - # Generate Random Users - num_students = 48 - num_assignments = 8 - num_teams = 16 - num_courses = 2 - num_instructors = 2 - - puts "creating instructors" - instructor_user_ids = [] - num_instructors.times do - instructor_user_ids << User.create( - name: Faker::Internet.unique.username, - email: Faker::Internet.unique.email, - password: "password", - full_name: Faker::Name.name, - institution_id: 1, - role_id: 3 - ).id - end - - puts "creating courses" - course_ids = [] - num_courses.times do |i| - course_ids << Course.create( - instructor_id: instructor_user_ids[i], - institution_id: inst_id, - directory_path: Faker::File.dir(segment_count: 2), - name: Faker::Company.industry, - info: "A fake class", - private: false - ).id - end - - puts "creating assignments" - assignment_ids = [] - num_assignments.times do |i| - assignment_ids << Assignment.create( - name: Faker::Verb.base, - instructor_id: instructor_user_ids[i % num_instructors], - course_id: course_ids[i % num_courses], - has_teams: true, - private: false - ).id - end - - puts "creating teams" - team_ids = [] - num_teams.times do |i| - team_ids << AssignmentTeam.create( - name: "Team #{i + 1}", - parent_id: assignment_ids[i % num_assignments] + #Create an instritution + inst_id = Institution.create!( + name: 'North Carolina State University', ).id - end - - puts "creating students" - student_user_ids = [] - num_students.times do - student_user_ids << User.create( - name: Faker::Internet.unique.username, - email: Faker::Internet.unique.email, - password: "password", - full_name: Faker::Name.name, - institution_id: 1, - role_id: 5, - parent_id: [nil, *instructor_user_ids].sample - ).id - end - - puts "assigning students to teams" - teams_users_ids = [] - # num_students.times do |i| - # teams_users_ids << TeamsUser.create( - # team_id: team_ids[i%num_teams], - # user_id: student_user_ids[i] - # ).id - # end - - num_students.times do |i| - puts "Creating TeamsUser with team_id: #{team_ids[i % num_teams]}, user_id: #{student_user_ids[i]}" - teams_user = TeamsUser.create( - team_id: team_ids[i % num_teams], - user_id: student_user_ids[i] + + roles = {} + + roles[:super_admin] = Role.find_or_create_by!(name: "Super Administrator", parent_id: nil) + + roles[:admin] = Role.find_or_create_by!(name: "Administrator", parent_id: roles[:super_admin].id) + + roles[:instructor] = Role.find_or_create_by!(name: "Instructor", parent_id: roles[:admin].id) + + roles[:ta] = Role.find_or_create_by!(name: "Teaching Assistant", parent_id: roles[:instructor].id) + + roles[:student] = Role.find_or_create_by!(name: "Student", parent_id: roles[:ta].id) + + puts "reached here" + # Create an admin user + User.create!( + name: 'admin', + email: 'admin2@example.com', + password: 'password123', + full_name: 'admin admin', + institution_id: inst_id, + role_id: roles[:super_admin].id ) - if teams_user.persisted? - teams_users_ids << teams_user.id - puts "Created TeamsUser with ID: #{teams_user.id}" - else - puts "Failed to create TeamsUser: #{teams_user.errors.full_messages.join(', ')}" - end - end - - puts "assigning participant to students, teams, courses, and assignments" - participant_ids = [] - num_students.times do |i| - participant_ids << AssignmentParticipant.create( - user_id: student_user_ids[i], - parent_id: assignment_ids[i%num_assignments], - team_id: team_ids[i%num_teams], - ).id - end + + # Create test student users student1..student5 for easy testing + (1..5).each do |i| + created_student = User.create!( + name: "student#{i}", + email: "student#{i}@test.com", + password: 'password123', + full_name: "Student #{i}", + institution_id: inst_id, + role_id: roles[:student].id + ) + puts "Created test student: #{created_student.email} with password: password123" + end + + + #Generate Random Users + num_students = 48 + num_assignments = 8 + num_teams = 16 + num_courses = 2 + num_instructors = 2 + + puts "creating instructors" + instructor_user_ids = [] + num_instructors.times do + instructor_user_ids << User.create( + name: Faker::Internet.unique.username, + email: Faker::Internet.unique.email, + password: "password", + full_name: Faker::Name.name, + institution_id: inst_id, + role_id: roles[:instructor].id + # Removed: type: 'Instructor' + ).id + end + + puts "creating courses" + course_ids = [] + num_courses.times do |i| + course_ids << Course.create( + instructor_id: instructor_user_ids[i], + institution_id: inst_id, + directory_path: Faker::File.dir(segment_count: 2), + name: Faker::Company.industry, + info: "A fake class", + private: false + ).id + end + + puts "creating assignments" + assignment_ids = [] + num_assignments.times do |i| + assignment_ids << Assignment.create( + name: Faker::Verb.base, + instructor_id: instructor_user_ids[i%num_instructors], + course_id: course_ids[i%num_courses], + has_teams: true, + private: false + ).id + end + + + puts "creating teams" + team_ids = [] + num_teams.times do |i| + team_ids << AssignmentTeam.create( + name: "Team #{i + 1}", + parent_id: assignment_ids[i%num_assignments], + type: 'AssignmentTeam' # This still needs 'type' for STI on the Team model + ).id + end + + puts "creating students" + student_user_ids = [] + num_students.times do + student_user_ids << User.create( + name: Faker::Internet.unique.username, + email: Faker::Internet.unique.email, + password: "password", + full_name: Faker::Name.name, + institution_id: inst_id, + role_id: roles[:student].id + # Removed: type: 'Student' + ).id + end + + puts "assigning students to teams (TeamsParticipant)" + teams_participant_ids = [] + num_students.times do |i| + team_id = team_ids[i % num_teams] + user_id = student_user_ids[i] + + # Participant must exist with the correct type before creating TeamsParticipant + participant = AssignmentParticipant.find_or_create_by!(user_id: user_id, parent_id: assignment_ids[i%num_assignments]) do |p| + p.team_id = team_id + p.type = 'AssignmentParticipant' # This still needs 'type' for STI on the Participant model + end + + tp = TeamsParticipant.create( + team_id: team_id, + user_id: user_id, + participant_id: participant.id + ) + if tp.persisted? + teams_participant_ids << tp.id + else + puts "Failed to create TeamsParticipant: #{tp.errors.full_messages.join(', ')}" + end + end + + puts "assigning participant to students, teams, courses, and assignments" + participant_ids = [] + num_students.times do |i| + participant_ids << AssignmentParticipant.find_or_create_by!( + user_id: student_user_ids[i], + parent_id: assignment_ids[i%num_assignments], + team_id: team_ids[i%num_teams] + ) do |p| + p.type = 'AssignmentParticipant' # This still needs 'type' for STI on the Participant model + end.id + end + + puts "creating project topics for testing" + if assignment_ids.any? + # Generate random topics for each assignment + assignment_ids.each do |assignment_id| + num_topics = rand(3..6) + + num_topics.times do |i| + # Ensure topic_identifier within 10 chars limit + identifier = "T" + Faker::Alphanumeric.alphanumeric(number: 5).upcase + ProjectTopic.create!( + topic_identifier: identifier, + topic_name: Faker::Educator.course_name, + category: Faker::Book.genre, + max_choosers: rand(2..5), + description: Faker::Lorem.sentence(word_count: 10), + link: Faker::Internet.url, + assignment_id: assignment_id + ) + end + puts "Created #{num_topics} topics for assignment #{assignment_id}" + end + end + + # ----------------------------------------------------------------------------- + # --- START: E2562 Review Grading Dashboard Seeding (Fixed for User model) ----- + # ----------------------------------------------------------------------------- + puts "\n--- Seeding data for E2562. Review grading dashboard ---" + + # 1. Create a dedicated Instructor (FIXED: Removed u.type = 'Instructor') + instructor = User.find_or_create_by!(name: 'instructor99') do |u| + u.email = 'instructor99@expertiza.edu' + u.password = 'password123' + u.full_name = 'E2562 Coordinator' + u.institution_id = inst_id + u.role_id = roles[:instructor].id + end + + # 2. Create the E2562 Assignment + assignment = Assignment.find_or_create_by!(name: 'E2562_Review_Dashboard', instructor: instructor) do |a| + a.has_teams = true + end + + # 3. Create a Review Questionnaire + puts "Seeding Review Questionnaire..." + review_questionnaire = Questionnaire.find_or_create_by!(name: 'Review Rubric', type: 'ReviewQuestionnaire') do |q| + q.max_question_score = 5 + q.min_question_score = 1 + q.instructor_id = instructor.id + end + + created_questions = [] + + # 4. Add questions to the questionnaire (Scale and Text Area) + created_questions << Scale.find_or_create_by!(questionnaire_id: review_questionnaire.id, txt: 'Technical merit (1-5)') do |q| + q.weight = 3 + q.type = 'Scale' + end + created_questions << TextArea.find_or_create_by!(questionnaire_id: review_questionnaire.id, txt: 'General Comments (Volume Metric)') do |q| + q.weight = 1 + q.type = 'TextArea' + end + + # 5. Link the questionnaire to the assignment + AssignmentQuestionnaire.find_or_create_by!(assignment: assignment, questionnaire: review_questionnaire, used_in_round: 1) + + # --- 6. Create Teams and Participants ----------------------------------------- + # Create Student Reviewers (FIXED: Removed u.type = 'Student') + num_reviewers = 4 + reviewers = [] + (1..num_reviewers).each do |i| + reviewers << User.find_or_create_by!(name: "e2562_reviewer_#{i}") do |u| + u.email = "e2562_reviewer_#{i}@expertiza.edu" + u.password = 'password123' + u.full_name = "E2562 Reviewer #{i}" + u.institution_id = inst_id + u.role_id = roles[:student].id + end + end + + # Create a Team to be Reviewed + team_to_be_reviewed = AssignmentTeam.find_or_create_by!(name: 'Target_Team_X', parent_id: assignment.id) do |t| + t.type = 'AssignmentTeam' + end + + # Create Participants for the assignment + reviewer_participants = reviewers.map do |user| + AssignmentParticipant.find_or_create_by!(assignment: assignment, user: user) do |p| + p.type = 'AssignmentParticipant' + end + end + + # --- 7. Create Reviews (ResponseMaps, Responses, Answers) --------------------- + puts "Creating Reviews (Responses)..." + + review_statuses = [ + { is_submitted: true, round: 1, comment: 'Excellent and thorough review. Very detailed comments on the architecture.' }, + { is_submitted: true, round: 1, comment: 'Good review. Needs more technical depth.' }, + { is_submitted: false, round: 1, comment: nil }, + { is_submitted: true, round: 1, comment: 'Solid review. Just a few words.' } + ] + + scale_question = created_questions.find { |q| q.type == 'Scale' } + text_area_question = created_questions.find { |q| q.type == 'TextArea' } + + reviewer_participants.each_with_index do |reviewer_participant, index| + status = review_statuses[index] + + # Create a ReviewResponseMap + review_map = ReviewResponseMap.find_or_create_by!( + reviewed_object_id: assignment.id, + reviewer_id: reviewer_participant.id, + reviewee_id: team_to_be_reviewed.id, + type: 'ReviewResponseMap' + ) + + # Create a Response for the map + if status[:is_submitted] + response = Response.find_or_create_by!(map_id: review_map.id, round: 1) do |r| + r.is_submitted = true + end + + # Create Answers for the Response + if scale_question + Answer.find_or_create_by!(response_id: response.id, question_id: scale_question.id) do |a| + a.answer = rand(3..scale_question.max_question_score) + end + end + + if text_area_question + Answer.find_or_create_by!(response_id: response.id, question_id: text_area_question.id) do |a| + a.comments = status[:comment] + end + end + end + end + + puts "Seeding complete. Created #{num_reviewers} reviews for Team: #{team_to_be_reviewed.name}" + # ----------------------------------------------------------------------------- + # --- END: E2562 Review Grading Dashboard Seeding ------------------------------ + # ----------------------------------------------------------------------------- rescue ActiveRecord::RecordInvalid => e - puts e, 'The db has already been seeded' -end + puts e.message + puts 'The db has already been seeded' +end \ No newline at end of file diff --git a/db/seeds_multi_round.rb b/db/seeds_multi_round.rb new file mode 100644 index 000000000..25911adfc --- /dev/null +++ b/db/seeds_multi_round.rb @@ -0,0 +1,90 @@ +# db/seeds_multi_round.rb + +# 1. Create Assignment +assignment = Assignment.find_or_create_by!(name: "Multi-Round Assignment") do |a| + a.directory_path = "multi_round_assignment" + a.submitter_count = 0 + a.course_id = 1 + a.instructor_id = 1 + a.rounds_of_reviews = 2 +end + +# 2. Create Questionnaires for Round 1 and Round 2 +q1 = Questionnaire.find_or_create_by!(name: "Round 1 Rubric") do |q| + q.instructor_id = 1 + q.private = false + q.min_question_score = 0 + q.max_question_score = 5 + q.questionnaire_type = "ReviewQuestionnaire" +end + +q2 = Questionnaire.find_or_create_by!(name: "Round 2 Rubric") do |q| + q.instructor_id = 1 + q.private = false + q.min_question_score = 0 + q.max_question_score = 5 + q.questionnaire_type = "ReviewQuestionnaire" +end + +# 3. Create Questions +question1 = Criterion.find_or_create_by!(txt: "Round 1 Question", questionnaire_id: q1.id) do |q| + q.weight = 1 + q.seq = 1 + q.question_type = "Criterion" + q.break_before = true + q.size = "50,3" +end + +question2 = Criterion.find_or_create_by!(txt: "Round 2 Question", questionnaire_id: q2.id) do |q| + q.weight = 1 + q.seq = 1 + q.question_type = "Criterion" + q.break_before = true + q.size = "50,3" +end + +# 4. Link Questionnaires to Assignment +AssignmentQuestionnaire.find_or_create_by!(assignment_id: assignment.id, questionnaire_id: q1.id) do |aq| + aq.notification_limit = 15 + aq.used_in_round = 1 +end + +AssignmentQuestionnaire.find_or_create_by!(assignment_id: assignment.id, questionnaire_id: q2.id) do |aq| + aq.notification_limit = 15 + aq.used_in_round = 2 +end + +# 5. Create Participants (Reviewer) +reviewer_user = User.find_or_create_by!(name: "reviewer5") do |u| + u.full_name = "Reviewer 5" + u.email = "reviewer5@example.com" + u.password = "password" + u.password_confirmation = "password" + u.role = Role.find_by(name: 'Student') + u.institution = Institution.first +end + +reviewer = Participant.find_or_create_by!(user_id: reviewer_user.id, parent_id: assignment.id) do |p| + p.type = 'AssignmentParticipant' + p.handle = "multi_handle" +end + +# 6. Create Team (Reviewee) +team = Team.find_or_create_by!(name: "Multi-Round Team", parent_id: assignment.id) do |t| + t.type = 'AssignmentTeam' +end + +# 7. Create Response Map +map = ReviewResponseMap.find_or_create_by!(reviewed_object_id: assignment.id, reviewer_id: reviewer.id, reviewee_id: team.id) + +# 8. Create Responses for Round 1 and Round 2 + +# Round 1 Response +resp1 = Response.create!(map_id: map.id, is_submitted: true, additional_comment: "Round 1 Feedback") +Answer.create!(response_id: resp1.id, question_id: question1.id, answer: 4, comments: "Good start") + +# Round 2 Response +resp2 = Response.create!(map_id: map.id, is_submitted: true, additional_comment: "Round 2 Feedback") +Answer.create!(response_id: resp2.id, question_id: question2.id, answer: 5, comments: "Excellent improvements") + +puts "Seeded Multi-Round Assignment ID: #{assignment.id}" diff --git a/db/seeds_review_dashboard.rb b/db/seeds_review_dashboard.rb new file mode 100644 index 000000000..eaf41ce5f --- /dev/null +++ b/db/seeds_review_dashboard.rb @@ -0,0 +1,87 @@ +# Find or create an assignment +assignment = Assignment.first || Assignment.create!(name: "Test Assignment", directory_path: "test_assignment", submitter_count: 0, course_id: 1, instructor_id: 1) + +# Create a Questionnaire +questionnaire = Questionnaire.find_or_create_by!(name: "Test Questionnaire") do |q| + q.instructor_id = 1 + q.private = false + q.min_question_score = 0 + q.max_question_score = 5 + q.questionnaire_type = "ReviewQuestionnaire" +end + +# Create a Criterion (Question) +question = Criterion.find_or_create_by!(txt: "Rate the work", questionnaire_id: questionnaire.id) do |q| + q.weight = 1 + q.seq = 1 + q.question_type = "Criterion" + q.break_before = true + q.size = "50,3" # Required by validation +end + +# Link Assignment and Questionnaire +AssignmentQuestionnaire.find_or_create_by!(assignment_id: assignment.id, questionnaire_id: questionnaire.id) do |aq| + aq.notification_limit = 15 +end + +# Create participants (Reviewers) +reviewers = [] +4.times do |i| + user = User.find_or_create_by!(name: "reviewer#{i+1}") do |u| + u.full_name = "Reviewer #{i+1}" + u.email = "reviewer#{i+1}@example.com" + u.password = "password" + u.password_confirmation = "password" + u.role = Role.find_by(name: 'Student') + u.institution = Institution.first + end + + # Check if participant already exists + participant = Participant.find_by(user_id: user.id, parent_id: assignment.id) + unless participant + participant = Participant.create!(user: user, parent_id: assignment.id, type: 'AssignmentParticipant', handle: "handle#{i+1}") + end + reviewers << participant +end + +# Create teams (Reviewees) +teams = [] +2.times do |i| + team = Team.find_or_create_by!(name: "Team #{i+1}", parent_id: assignment.id) do |t| + t.type = 'AssignmentTeam' + end + teams << team +end + +# Create ReviewResponseMaps and Responses + +# Case 1: Completed review with grade (Brown) +map1 = ReviewResponseMap.find_or_create_by!(reviewed_object_id: assignment.id, reviewer_id: reviewers[0].id, reviewee_id: teams[0].id) +map1.update!(reviewer_grade: 90, reviewer_comment: "Good job") +resp1 = Response.find_or_create_by!(map_id: map1.id) do |r| + r.is_submitted = true + r.additional_comment = "This is a great submission with lots of details." +end +Answer.create!(response_id: resp1.id, question_id: question.id, answer: 5, comments: "Excellent") unless Answer.exists?(response_id: resp1.id, question_id: question.id) + + +# Case 2: Completed review, no grade (Blue) +map2 = ReviewResponseMap.find_or_create_by!(reviewed_object_id: assignment.id, reviewer_id: reviewers[1].id, reviewee_id: teams[0].id) +resp2 = Response.find_or_create_by!(map_id: map2.id) do |r| + r.is_submitted = true + r.additional_comment = "Good work but needs improvement." +end +Answer.create!(response_id: resp2.id, question_id: question.id, answer: 3, comments: "Average") unless Answer.exists?(response_id: resp2.id, question_id: question.id) + +# Case 3: Not completed (Red) +map3 = ReviewResponseMap.find_or_create_by!(reviewed_object_id: assignment.id, reviewer_id: reviewers[2].id, reviewee_id: teams[0].id) +# No response or not submitted +Response.find_or_create_by!(map_id: map3.id) do |r| + r.is_submitted = false +end + +# Case 4: No review (Purple) +ReviewResponseMap.find_or_create_by!(reviewed_object_id: assignment.id, reviewer_id: reviewers[3].id, reviewee_id: teams[1].id) +# No response object created + +puts "Seeded review data for Assignment ID: #{assignment.id}" diff --git a/debug_scores.rb b/debug_scores.rb new file mode 100644 index 000000000..469ad3e00 --- /dev/null +++ b/debug_scores.rb @@ -0,0 +1,40 @@ +# Script to debug score calculation +assignment = Assignment.first +puts "Assignment: #{assignment.name} (ID: #{assignment.id})" + +# Get the first review response map +map = ReviewResponseMap.where(reviewed_object_id: assignment.id).first +puts "Map ID: #{map.id}" + +# Get the response +response = Response.where(map_id: map.id).order(created_at: :desc).first +puts "Response ID: #{response.id}" + +# Check scores (Answers) +scores = response.scores +puts "Scores count: #{scores.count}" + +scores.each do |s| + item = Item.find_by(id: s.question_id) + if item + puts " Answer: #{s.answer}, Question ID: #{s.question_id}, Item Type: #{item.question_type}, Weight: #{item.weight}, Scorable: #{item.scorable?}" + else + puts " Answer: #{s.answer}, Question ID: #{s.question_id} - ITEM NOT FOUND" + end +end + +# Calculate score manually +sum = 0 +scores.each do |s| + item = Item.find_by(id: s.question_id) + if item && !s.answer.nil? && item.scorable? + puts " Adding #{s.answer} * #{item.weight} = #{s.answer * item.weight}" + sum += s.answer * item.weight + else + puts " Skipping: Answer nil? #{s.answer.nil?}, Item found? #{!item.nil?}, Scorable? #{item&.scorable?}" + end +end +puts "Manual Sum: #{sum}" + +# Call the method +puts "Method Result: #{response.aggregate_questionnaire_score}" diff --git a/seed_output.txt b/seed_output.txt new file mode 100644 index 000000000..a830f1330 --- /dev/null +++ b/seed_output.txt @@ -0,0 +1,35 @@ +/usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/validations.rb:87:in 'ActiveRecord::Validations#raise_validation_error': Validation failed: Item must exist (ActiveRecord::RecordInvalid) + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/validations.rb:54:in 'ActiveRecord::Validations#save!' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/transactions.rb:365:in 'block in ActiveRecord::Transactions#save!' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/transactions.rb:417:in 'block (2 levels) in ActiveRecord::Transactions#with_transaction_returning_status' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/connection_adapters/abstract/transaction.rb:626:in 'block in ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction' + from /usr/local/bundle/gems/activesupport-8.0.3/lib/active_support/concurrency/null_lock.rb:9:in 'ActiveSupport::Concurrency::NullLock#synchronize' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/connection_adapters/abstract/transaction.rb:623:in 'ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/connection_adapters/abstract/database_statements.rb:367:in 'ActiveRecord::ConnectionAdapters::DatabaseStatements#within_new_transaction' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/connection_adapters/abstract/database_statements.rb:359:in 'ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/transactions.rb:413:in 'block in ActiveRecord::Transactions#with_transaction_returning_status' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/connection_adapters/abstract/connection_pool.rb:422:in 'ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/connection_handling.rb:310:in 'ActiveRecord::ConnectionHandling#with_connection' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/transactions.rb:409:in 'ActiveRecord::Transactions#with_transaction_returning_status' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/transactions.rb:365:in 'ActiveRecord::Transactions#save!' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/suppressor.rb:56:in 'ActiveRecord::Suppressor#save!' + from /usr/local/bundle/gems/activerecord-8.0.3/lib/active_record/persistence.rb:55:in 'ActiveRecord::Persistence::ClassMethods#create!' + from /app/db/seeds_review_dashboard.rb:65:in '
' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/commands/runner/runner_command.rb:44:in 'Kernel.load' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/commands/runner/runner_command.rb:44:in 'block in Rails::Command::RunnerCommand#perform' + from /usr/local/bundle/gems/activesupport-8.0.3/lib/active_support/execution_wrapper.rb:91:in 'ActiveSupport::ExecutionWrapper.wrap' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/commands/runner/runner_command.rb:70:in 'Rails::Command::RunnerCommand#conditional_executor' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/commands/runner/runner_command.rb:43:in 'Rails::Command::RunnerCommand#perform' + from /usr/local/bundle/gems/thor-1.4.0/lib/thor/command.rb:28:in 'Thor::Command#run' + from /usr/local/bundle/gems/thor-1.4.0/lib/thor/invocation.rb:127:in 'Thor::Invocation#invoke_command' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/command/base.rb:178:in 'Rails::Command::Base#invoke_command' + from /usr/local/bundle/gems/thor-1.4.0/lib/thor.rb:538:in 'Thor.dispatch' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/command/base.rb:73:in 'Rails::Command::Base.perform' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/command.rb:65:in 'block in Rails::Command.invoke' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/command.rb:143:in 'Rails::Command.with_argv' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/command.rb:63:in 'Rails::Command.invoke' + from /usr/local/bundle/gems/railties-8.0.3/lib/rails/commands.rb:18:in '
' + from :37:in 'Kernel#require' + from :37:in 'Kernel#require' + from /usr/local/bundle/gems/bootsnap-1.18.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in 'Kernel#require' + from bin/rails:4:in '
'