From f5abeaaf4bf579f93cffed5879c6a408c9ae5f63 Mon Sep 17 00:00:00 2001 From: pandurk Date: Fri, 7 Mar 2025 13:37:59 -0500 Subject: [PATCH 01/41] Initial commit on grades_controller --- app/controllers/api/v1/grades_controller.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/controllers/api/v1/grades_controller.rb diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb new file mode 100644 index 000000000..e69de29bb From 7e1d2e7a23e4da0f84b9e06f90f883616bc373d7 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sun, 16 Mar 2025 13:50:48 -0400 Subject: [PATCH 02/41] Updated ReadMe with run commands --- README.md | 13 +++++++++++++ config/application.rb | 1 + 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index 6c91b930b..4d189d048 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,19 @@ Things you may want to cover: - Make sure that the Docker plugin [is enabled](https://www.jetbrains.com/help/ruby/docker.html#enable_docker). +### To Run the application +#### On one terminal: + +- docker-compose build --no-cache +- docker-compose up + +#### On another terminal: + +- docker exec -it reimplementation-back-end-app-1 bash +- rake db:migrate:reset +- rake db:seed + + ### Instructions Tutorial: [Docker Compose as a remote interpreter](https://www.jetbrains.com/help/ruby/using-docker-compose-as-a-remote-interpreter.html) diff --git a/config/application.rb b/config/application.rb index 4bd4ca23e..c66af389b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,6 +16,7 @@ module Reimplementation class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 + config.active_record.schema_format = :ruby # Configuration for the application, engines, and railties goes here. # From 186230c6fbbcc8d2a72a77aa3f5815af5f66fbad Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Sun, 16 Mar 2025 22:53:09 -0400 Subject: [PATCH 03/41] edit, update, update_team, list_questions completed for round 1 --- app/controllers/api/v1/grades_controller.rb | 98 +++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index e69de29bb..8851ff653 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -0,0 +1,98 @@ +class Api::V1::GradesController < ApplicationController + include AuthorizationHelper + + def action_allowed? + permitted = case params[:action] + when 'view_team' + has_role?('Student') + else + has_privileges_of?('Teaching Assistant') + end + render json: { allowed: permitted }, status: permitted ? :ok : :forbidden + end + + + def edit + @participant = AssignmentParticipant.find(params[:id]) + if @participant.nil? + render json: { message: "Assignment participant #{params[:id]} not found" }, status: :not_found + return + end + @assignment = @participant.assignment + @questions = list_questions(@assignment) + @scores = participant_scores(@participant, @questions) + end + + def list_questions(assignment) + assignment.questionnaires.each_with_object({}) do |questionnaire, questions| + questions[questionnaire.id.to_s] = questionnaire.questions + end + end + + def update + participant = AssignmentParticipant.find_by(id: params[:id]) + return handle_not_found unless participant + + new_grade = params[:participant][:grade] + if grade_changed?(participant, new_grade) + participant.update(grade: new_grade) + flash[:note] = grade_message(participant) + end + redirect_to action: 'edit', id: params[:id] + end + + def update_team + participant = AssignmentParticipant.find_by(id: params[:participant_id]) + return handle_not_found unless participant + + if participant.team.update(grade_for_submission: params[:grade_for_submission], + comment_for_submission: params[:comment_for_submission]) + flash[:success] = 'Grade and comment for submission successfully saved.' + else + flash[:error] = 'Error saving grade and comment.' + end + redirect_to controller: 'grades', action: 'view_team', id: params[:id] + end + + private + + def handle_not_found + flash[:error] = 'Participant not found.' + end + + def grade_changed?(participant, new_grade) + return false if new_grade.nil? + + format('%.2f', params[:total_score]) != new_grade + end + + def grade_message(participant) + participant.grade.nil? ? "The computed score will be used for #{participant.user.name}." : + "A score of #{participant.grade}% has been saved for #{participant.user.name}." + end +end + + +def filter_questionnaires(assignment) + questionnaires = assignment.questionnaires + if assignment.varying_rubrics_by_round? + retrieve_questions(questionnaires, assignment.id) + else + questions = {} + questionnaires.each do |questionnaire| + questions[questionnaire.id.to_s.to_sym] = questionnaire.questions + end + questions + end +end + +def get_data_for_heat_map(id) + # Finds the assignment + @assignment = Assignment.find(id) + # Extracts the questionnaires + @questions = filter_questionnaires(@assignment) + @scores = review_grades(@assignment, @questions) + @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash + @averages = filter_scores(@scores[:teams]) + @avg_of_avg = mean(@averages) +end \ No newline at end of file From 75d6c9eda1e28414a163b8eb2a08946bf8594039 Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Sun, 16 Mar 2025 22:53:24 -0400 Subject: [PATCH 04/41] Gemfile commit --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 462e09123..792dd1ba1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.2.7' +ruby '3.3.6' gem 'mysql2', '~> 0.5.5' gem 'puma', '~> 5.0' From f2cbf0fb88816d0cbe20d5320c91e0be02be0102 Mon Sep 17 00:00:00 2001 From: chrisandrews1012 Date: Sun, 16 Mar 2025 22:57:08 -0400 Subject: [PATCH 05/41] Removed unfinished work for the main branch --- app/controllers/api/v1/grades_controller.rb | 36 --------------------- 1 file changed, 36 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 8851ff653..b58cdd34c 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,17 +1,6 @@ class Api::V1::GradesController < ApplicationController include AuthorizationHelper - def action_allowed? - permitted = case params[:action] - when 'view_team' - has_role?('Student') - else - has_privileges_of?('Teaching Assistant') - end - render json: { allowed: permitted }, status: permitted ? :ok : :forbidden - end - - def edit @participant = AssignmentParticipant.find(params[:id]) if @participant.nil? @@ -71,28 +60,3 @@ def grade_message(participant) "A score of #{participant.grade}% has been saved for #{participant.user.name}." end end - - -def filter_questionnaires(assignment) - questionnaires = assignment.questionnaires - if assignment.varying_rubrics_by_round? - retrieve_questions(questionnaires, assignment.id) - else - questions = {} - questionnaires.each do |questionnaire| - questions[questionnaire.id.to_s.to_sym] = questionnaire.questions - end - questions - end -end - -def get_data_for_heat_map(id) - # Finds the assignment - @assignment = Assignment.find(id) - # Extracts the questionnaires - @questions = filter_questionnaires(@assignment) - @scores = review_grades(@assignment, @questions) - @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash - @averages = filter_scores(@scores[:teams]) - @avg_of_avg = mean(@averages) -end \ No newline at end of file From 691a09b9a836822fc3f62802ad17379872312f89 Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Mon, 17 Mar 2025 11:32:08 -0400 Subject: [PATCH 06/41] action_allowed and instructor_review implemented --- app/controllers/api/v1/grades_controller.rb | 77 ++++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 8851ff653..7e15c2774 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -8,16 +8,12 @@ def action_allowed? else has_privileges_of?('Teaching Assistant') end - render json: { allowed: permitted }, status: permitted ? :ok : :forbidden + render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end - def edit - @participant = AssignmentParticipant.find(params[:id]) - if @participant.nil? - render json: { message: "Assignment participant #{params[:id]} not found" }, status: :not_found - return - end + @participant = find_participant(params[:id]) + return unless @participant # Exit early if participant not found @assignment = @participant.assignment @questions = list_questions(@assignment) @scores = participant_scores(@participant, @questions) @@ -54,7 +50,44 @@ def update_team redirect_to controller: 'grades', action: 'view_team', id: params[:id] end + + def instructor_review + participant = find_participant(params[:id]) + return unless participant # Exit early if participant not found + + reviewer = find_or_create_reviewer(session[:user].id, participant.assignment.id) + review_mapping = find_or_create_review_mapping(participant.team.id, reviewer.id, participant.assignment.id) + + redirect_to_review(review_mapping) + end + private + + def find_participant(participant_id) + AssignmentParticipant.find(participant_id) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Assignment participant #{participant_id} not found" + nil + end + + def find_or_create_reviewer(user_id, assignment_id) + reviewer = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) + reviewer.set_handle if reviewer.new_record? + reviewer + end + + def find_or_create_review_mapping(reviewee_id, reviewer_id, assignment_id) + ReviewResponseMap.find_or_create_by(reviewee_id: reviewee_id, reviewer_id: reviewer_id, reviewed_object_id: assignment_id) + end + + def redirect_to_review(review_mapping) + if review_mapping.new_record? + redirect_to controller: 'response', action: 'new', id: review_mapping.map_id, return: 'instructor' + else + review = Response.find_by(map_id: review_mapping.map_id) + redirect_to controller: 'response', action: 'edit', id: review.id, return: 'instructor' + end + end def handle_not_found flash[:error] = 'Participant not found.' @@ -73,6 +106,13 @@ def grade_message(participant) end +def find_assignment(assignment_id) + AssignmentParticipant.find(assignment_id) +rescue ActiveRecord::RecordNotFound + flash[:error] = "Assignment participant #{assignment_id} not found" + nil +end + def filter_questionnaires(assignment) questionnaires = assignment.questionnaires if assignment.varying_rubrics_by_round? @@ -86,13 +126,32 @@ def filter_questionnaires(assignment) end end -def get_data_for_heat_map(id) +def get_data_for_heat_map(assignment_id) # Finds the assignment - @assignment = Assignment.find(id) + @assignment = find_assignment(assignment_id) # Extracts the questionnaires @questions = filter_questionnaires(@assignment) @scores = review_grades(@assignment, @questions) @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash @averages = filter_scores(@scores[:teams]) @avg_of_avg = mean(@averages) +end + +def self_review_finished? + participant = Participant.find(params[:id]) + assignment = participant.try(:assignment) + self_review_enabled = assignment.try(:is_selfreview_enabled) + not_submitted = unsubmitted_self_review?(participant.try(:id)) + if self_review_enabled + !not_submitted + else + true + end +end + +def unsubmitted_self_review?(participant_id) + self_review = SelfReviewResponseMap.where(reviewer_id: participant_id).first.try(:response).try(:last) + return !self_review.try(:is_submitted) if self_review + + true end \ No newline at end of file From 488ef51efcd3b579cdd29ca7ecebc0431e41ab6c Mon Sep 17 00:00:00 2001 From: chrisandrews1012 Date: Mon, 17 Mar 2025 11:34:11 -0400 Subject: [PATCH 07/41] Update grades_controller.rb --- app/controllers/api/v1/grades_controller.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 3dc402c0b..3f6696c37 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,7 +1,5 @@ class Api::V1::GradesController < ApplicationController include AuthorizationHelper - -<<<<<<< HEAD def action_allowed? permitted = case params[:action] when 'view_team' @@ -12,8 +10,6 @@ def action_allowed? render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end -======= ->>>>>>> f2cbf0fb88816d0cbe20d5320c91e0be02be0102 def edit @participant = find_participant(params[:id]) return unless @participant # Exit early if participant not found @@ -107,8 +103,6 @@ def grade_message(participant) "A score of #{participant.grade}% has been saved for #{participant.user.name}." end end -<<<<<<< HEAD - def find_assignment(assignment_id) AssignmentParticipant.find(assignment_id) @@ -159,5 +153,3 @@ def unsubmitted_self_review?(participant_id) true end -======= ->>>>>>> f2cbf0fb88816d0cbe20d5320c91e0be02be0102 From 5a228062470e796af260ad346fdefee12843bf04 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 17 Mar 2025 12:01:49 -0400 Subject: [PATCH 08/41] Added self_review_response_map model --- Gemfile | 2 +- app/models/self_review_response_map.rb | 22 ++++++++++++++++++++ spec/factories.rb | 4 ++++ spec/models/self_review_response_map_spec.rb | 5 +++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 app/models/self_review_response_map.rb create mode 100644 spec/models/self_review_response_map_spec.rb diff --git a/Gemfile b/Gemfile index 792dd1ba1..462e09123 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.3.6' +ruby '3.2.7' gem 'mysql2', '~> 0.5.5' gem 'puma', '~> 5.0' diff --git a/app/models/self_review_response_map.rb b/app/models/self_review_response_map.rb new file mode 100644 index 000000000..0ed31c70b --- /dev/null +++ b/app/models/self_review_response_map.rb @@ -0,0 +1,22 @@ +class SelfReviewResponseMap < ResponseMap + belongs_to :reviewee, class_name: 'Team', foreign_key: 'reviewee_id' + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'reviewed_object_id' + + # Find a review questionnaire associated with this self-review response map's assignment + def questionnaire(round_number = nil, topic_id = nil) + Questionnaire.find(assignment.review_questionnaire_id(round_number, topic_id)) + end + + # This method helps to find contributor - here Team ID + def contributor + Team.find_by(id: reviewee_id) + end + + # This method returns 'Title' of type of review (used to manipulate headings accordingly) + def get_title + 'Self Review' + end + + # do not send any reminder for self review received. + def email(defn, participant, assignment); end +end diff --git a/spec/factories.rb b/spec/factories.rb index 758fa51a2..5a412e475 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,4 +1,8 @@ FactoryBot.define do + factory :self_review_response_map do + + end + factory :student_task do assignment { nil } current_stage { "MyString" } diff --git a/spec/models/self_review_response_map_spec.rb b/spec/models/self_review_response_map_spec.rb new file mode 100644 index 000000000..d2114182f --- /dev/null +++ b/spec/models/self_review_response_map_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SelfReviewResponseMap, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 3ee7eece6fb82447b59476dd4ad3a045a001a456 Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Mon, 17 Mar 2025 12:03:00 -0400 Subject: [PATCH 09/41] implemented self_review_finished? and are_needed_authorizations_present? --- app/controllers/api/v1/grades_controller.rb | 119 +++++++++++--------- app/models/response_map.rb | 8 ++ 2 files changed, 73 insertions(+), 54 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 3dc402c0b..11667a12c 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,19 +1,19 @@ class Api::V1::GradesController < ApplicationController include AuthorizationHelper -<<<<<<< HEAD def action_allowed? permitted = case params[:action] + when 'view_my_scores' + has_role?('Student') && self_review_finished? && are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') when 'view_team' - has_role?('Student') + + else has_privileges_of?('Teaching Assistant') end render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end -======= ->>>>>>> f2cbf0fb88816d0cbe20d5320c91e0be02be0102 def edit @participant = find_participant(params[:id]) return unless @participant # Exit early if participant not found @@ -106,58 +106,69 @@ def grade_message(participant) participant.grade.nil? ? "The computed score will be used for #{participant.user.name}." : "A score of #{participant.grade}% has been saved for #{participant.user.name}." end -end -<<<<<<< HEAD - - -def find_assignment(assignment_id) - AssignmentParticipant.find(assignment_id) -rescue ActiveRecord::RecordNotFound - flash[:error] = "Assignment participant #{assignment_id} not found" - nil -end - -def filter_questionnaires(assignment) - questionnaires = assignment.questionnaires - if assignment.varying_rubrics_by_round? - retrieve_questions(questionnaires, assignment.id) - else - questions = {} - questionnaires.each do |questionnaire| - questions[questionnaire.id.to_s.to_sym] = questionnaire.questions + + + + def find_assignment(assignment_id) + AssignmentParticipant.find(assignment_id) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Assignment participant #{assignment_id} not found" + nil + end + + def filter_questionnaires(assignment) + questionnaires = assignment.questionnaires + if assignment.varying_rubrics_by_round? + retrieve_questions(questionnaires, assignment.id) + else + questions = {} + questionnaires.each do |questionnaire| + questions[questionnaire.id.to_s.to_sym] = questionnaire.questions + end + questions + end + end + + def get_data_for_heat_map(assignment_id) + # Finds the assignment + @assignment = find_assignment(assignment_id) + # Extracts the questionnaires + @questions = filter_questionnaires(@assignment) + @scores = review_grades(@assignment, @questions) + @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash + @averages = filter_scores(@scores[:teams]) + @avg_of_avg = mean(@averages) + end + + def self_review_finished? + participant = Participant.find(params[:id]) + assignment = participant.try(:assignment) + self_review_enabled = assignment.try(:is_selfreview_enabled) + not_submitted = unsubmitted_self_review?(participant.try(:id)) + if self_review_enabled + !not_submitted + else + true end - questions - end -end - -def get_data_for_heat_map(assignment_id) - # Finds the assignment - @assignment = find_assignment(assignment_id) - # Extracts the questionnaires - @questions = filter_questionnaires(@assignment) - @scores = review_grades(@assignment, @questions) - @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash - @averages = filter_scores(@scores[:teams]) - @avg_of_avg = mean(@averages) -end - -def self_review_finished? - participant = Participant.find(params[:id]) - assignment = participant.try(:assignment) - self_review_enabled = assignment.try(:is_selfreview_enabled) - not_submitted = unsubmitted_self_review?(participant.try(:id)) - if self_review_enabled - !not_submitted - else + end + + def unsubmitted_self_review?(participant_id) + self_review = SelfReviewResponseMap.where(reviewer_id: participant_id).first.try(:response).try(:last) + return !self_review.try(:is_submitted) if self_review + true end -end -def unsubmitted_self_review?(participant_id) - self_review = SelfReviewResponseMap.where(reviewer_id: participant_id).first.try(:response).try(:last) - return !self_review.try(:is_submitted) if self_review + def self_review_finished? + participant = Participant.find(params[:id]) + assignment = participant.try(:assignment) + self_review_enabled = assignment.try(:is_selfreview_enabled) + not_submitted = unsubmitted_self_review?(participant.try(:id)) + if self_review_enabled + !not_submitted + else + true + end + end + - true -end -======= ->>>>>>> f2cbf0fb88816d0cbe20d5320c91e0be02be0102 diff --git a/app/models/response_map.rb b/app/models/response_map.rb index d27124d76..d1e88e74b 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -38,4 +38,12 @@ def self.assessments_for(team) end responses end + + def unsubmitted_self_review?(participant_id) + self_review = SelfReviewResponseMap.where(reviewer_id: participant_id).first.try(:response).try(:last) + return !self_review.try(:is_submitted) if self_review + + true + end + end From 5a337c7c526d36dd0f3899c5005f0dbea16d3759 Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Mon, 17 Mar 2025 13:18:23 -0400 Subject: [PATCH 10/41] Added helper methods for View actions and finalized action_allowed? with associated functionality --- app/controllers/api/v1/grades_controller.rb | 126 +++++++++++++------- app/controllers/concerns/authorization.rb | 15 ++- 2 files changed, 94 insertions(+), 47 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 19ef3eaf7..bc106a96e 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -3,30 +3,42 @@ class Api::V1::GradesController < ApplicationController def action_allowed? permitted = case params[:action] when 'view_my_scores' - has_role?('Student') && self_review_finished? && are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') + student_with_permissions? when 'view_team' - - + student_viewing_own_team? || has_privileges_of?('Teaching Assistant') else has_privileges_of?('Teaching Assistant') end + render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end + + + + def edit @participant = find_participant(params[:id]) return unless @participant # Exit early if participant not found @assignment = @participant.assignment @questions = list_questions(@assignment) - @scores = participant_scores(@participant, @questions) + @scores = Response.review_grades(@participant, @questions) end + + + + def list_questions(assignment) assignment.questionnaires.each_with_object({}) do |questionnaire, questions| questions[questionnaire.id.to_s] = questionnaire.questions end end + + + + def update participant = AssignmentParticipant.find_by(id: params[:id]) return handle_not_found unless participant @@ -39,6 +51,11 @@ def update redirect_to action: 'edit', id: params[:id] end + + + + + def update_team participant = AssignmentParticipant.find_by(id: params[:participant_id]) return handle_not_found unless participant @@ -53,6 +70,10 @@ def update_team end + + + + def instructor_review participant = find_participant(params[:id]) return unless participant # Exit early if participant not found @@ -63,21 +84,49 @@ def instructor_review redirect_to_review(review_mapping) end + + + private - - def find_participant(participant_id) - AssignmentParticipant.find(participant_id) - rescue ActiveRecord::RecordNotFound - flash[:error] = "Assignment participant #{participant_id} not found" - nil + + + +# Helper methods for action_allowed? + + def student_with_permissions? + has_role?('Student') && + self_review_finished? && + are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') end + + def student_viewing_own_team? + return false unless has_role?('Student') + participant = AssignmentParticipant.find_by(id: params[:id]) + participant && current_user_is_assignment_participant?(participant.assignment.id) + end + + def self_review_finished? + participant = Participant.find(params[:id]) + assignment = participant.try(:assignment) + self_review_enabled = assignment.try(:is_selfreview_enabled) + not_submitted = ResponseMap.unsubmitted_self_review?(participant.try(:id)) + if self_review_enabled + !not_submitted + else + true + end + end + + +# Helper methods for the instructor_review + def find_or_create_reviewer(user_id, assignment_id) reviewer = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) reviewer.set_handle if reviewer.new_record? reviewer end - + def find_or_create_review_mapping(reviewee_id, reviewer_id, assignment_id) ReviewResponseMap.find_or_create_by(reviewee_id: reviewee_id, reviewer_id: reviewer_id, reviewed_object_id: assignment_id) end @@ -91,6 +140,10 @@ def redirect_to_review(review_mapping) end end + + +# Helper methods for update + def handle_not_found flash[:error] = 'Participant not found.' end @@ -106,14 +159,27 @@ def grade_message(participant) "A score of #{participant.grade}% has been saved for #{participant.user.name}." end +# These could go in a helper method + + def find_participant(participant_id) + AssignmentParticipant.find(participant_id) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Assignment participant #{participant_id} not found" + nil + end + def find_assignment(assignment_id) - AssignmentParticipant.find(assignment_id) + Assignment.find(assignment_id) rescue ActiveRecord::RecordNotFound flash[:error] = "Assignment participant #{assignment_id} not found" nil end + +# Helper methods for views + + def filter_questionnaires(assignment) questionnaires = assignment.questionnaires if assignment.varying_rubrics_by_round? @@ -132,46 +198,14 @@ def get_data_for_heat_map(assignment_id) @assignment = find_assignment(assignment_id) # Extracts the questionnaires @questions = filter_questionnaires(@assignment) - @scores = review_grades(@assignment, @questions) + @scores = Response.review_grades(@assignment, @questions) @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash @averages = filter_scores(@scores[:teams]) @avg_of_avg = mean(@averages) end - def self_review_finished? - participant = Participant.find(params[:id]) - assignment = participant.try(:assignment) - self_review_enabled = assignment.try(:is_selfreview_enabled) - not_submitted = unsubmitted_self_review?(participant.try(:id)) - if self_review_enabled - !not_submitted - else - true - end - end - - def unsubmitted_self_review?(participant_id) - self_review = SelfReviewResponseMap.where(reviewer_id: participant_id).first.try(:response).try(:last) - return !self_review.try(:is_submitted) if self_review - true - end - def self_review_finished? - participant = Participant.find(params[:id]) - assignment = participant.try(:assignment) - self_review_enabled = assignment.try(:is_selfreview_enabled) - not_submitted = unsubmitted_self_review?(participant.try(:id)) - if self_review_enabled - !not_submitted - else - true - end - end +end -<<<<<<< HEAD -======= - true -end ->>>>>>> 5a228062470e796af260ad346fdefee12843bf04 diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index 8450748cc..c7bb78cea 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -47,4 +47,17 @@ def has_role?(required_role) required_role = required_role.name if required_role.is_a?(Role) current_user&.role&.name == required_role end -end \ No newline at end of file + + def are_needed_authorizations_present?(id, *authorizations) + authorization = Participant.find_by(id: id)&.authorization + authorization.present? && !authorizations.include?(authorization) + end + + # Check if the currently logged-in user is a participant in an assignment + def current_user_is_assignment_participant?(assignment_id) + return false unless session[:user] + + AssignmentParticipant.exists?(parent_id: assignment_id, user_id: session[:user].id) + end +end + From e063f9934eec7f623c4cd5cd86ce71437a8fd89f Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 17 Mar 2025 14:29:06 -0400 Subject: [PATCH 11/41] Added helpers for grades and penalties --- app/helpers/grades_helper.rb | 3 +++ app/helpers/penalty_helper.rb | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 app/helpers/grades_helper.rb create mode 100644 app/helpers/penalty_helper.rb diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb new file mode 100644 index 000000000..9aa1958f0 --- /dev/null +++ b/app/helpers/grades_helper.rb @@ -0,0 +1,3 @@ +module GradesHelper + include PenaltyHelper +end \ No newline at end of file diff --git a/app/helpers/penalty_helper.rb b/app/helpers/penalty_helper.rb new file mode 100644 index 000000000..9b974df20 --- /dev/null +++ b/app/helpers/penalty_helper.rb @@ -0,0 +1,3 @@ +module PenaltyHelper + +end \ No newline at end of file From 1ae332399464de60b2e1799cb3868a642d2bcfa3 Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Mon, 17 Mar 2025 16:20:22 -0400 Subject: [PATCH 12/41] Finished the three view methods and came up with questions to ask the TA tmrw during our meeting --- app/controllers/api/v1/grades_controller.rb | 48 ++- app/helpers/grades_helper.rb | 111 +++++- app/helpers/penalty_helper.rb | 106 ++++++ app/models/response.rb | 364 ++++++++++++++++++++ 4 files changed, 625 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index bc106a96e..3dde2f5a8 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,5 +1,7 @@ class Api::V1::GradesController < ApplicationController include AuthorizationHelper + include GradesHelper + def action_allowed? permitted = case params[:action] when 'view_my_scores' @@ -13,9 +15,48 @@ def action_allowed? render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end + def view + get_data_for_heat_map(params[:id]) + fetch_penalties + @show_reputation = false + + + + def view_my_scores + fetch_participant_and_assignment + @team_id = TeamsUser.team_id(@participant.parent_id, @participant.user_id) + return if redirect_when_disallowed + + fetch_questionnaires_and_questions + fetch_participant_scores + # get_data_for_heat_map() + + @topic_id = SignedUpTeam.topic_id(@participant.assignment.id, @participant.user_id) + @stage = @participant.assignment.current_stage(@topic_id) + fetch_penalties + + # prepare feedback summaries + fetch_feedback_summary + end + + def view_team + fetch_participant_and_assignment + @team = @participant.team + @team_id = @team.id + + questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: nil).map(&:questionnaire) + @questions = retrieve_questions(questionnaires, @assignment.id) + @pscore = Response.participant_scores(@participant, @questions) + @penalties = calculate_penalty(@participant.id) + @vmlist = process_questionare_for_team(@assignment, @team_id) + + @current_role_name = current_role_name + end + + def edit @participant = find_participant(params[:id]) @@ -199,13 +240,14 @@ def get_data_for_heat_map(assignment_id) # Extracts the questionnaires @questions = filter_questionnaires(@assignment) @scores = Response.review_grades(@assignment, @questions) - @review_score_count = @scores[:participants].length # After rejecting nil scores need original length to iterate over hash - @averages = filter_scores(@scores[:teams]) - @avg_of_avg = mean(@averages) + @review_score_count = @scores[:teams].length # After rejecting nil scores need original length to iterate over hash + @averages = Response.extract_team_averages(@scores[:teams]) + @avg_of_avg = Response.average_team_scores(@averages) end + end diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index 9aa1958f0..b923fae3c 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -1,3 +1,112 @@ module GradesHelper - include PenaltyHelper + include PenaltyHelper + + # This function calculates all the penalties + def penalties(assignment_id) + @all_penalties = {} + @assignment = Assignment.find(assignment_id) + calculate_for_participants = true unless @assignment.is_penalty_calculated + Participant.where(parent_id: assignment_id).each do |participant| + penalties = calculate_penalty(participant.id) + @total_penalty = 0 + + unless penalties[:submission].zero? || penalties[:review].zero? || penalties[:meta_review].zero? + + @total_penalty = (penalties[:submission] + penalties[:review] + penalties[:meta_review]) + l_policy = LatePolicy.find(@assignment.late_policy_id) + @total_penalty = l_policy.max_penalty if @total_penalty > l_policy.max_penalty + attributes(@participant) if calculate_for_participants + end + assign_all_penalties(participant, penalties) + end + @assignment[:is_penalty_calculated] = true unless @assignment.is_penalty_calculated + end + + + # Helper to retrieve participant and related assignment data +def fetch_participant_and_assignment + @participant = AssignmentParticipant.find(params[:id]) + @assignment = @participant.assignment +end + +# Helper to retrieve questionnaires and questions +def fetch_questionnaires_and_questions + questionnaires = @assignment.questionnaires + @questions = retrieve_questions(questionnaires, @assignment.id) +end + +# Helper to fetch participant scores +def fetch_participant_scores + @pscore = participant_scores(@participant, @questions) +end + +# Helper to calculate penalties +def fetch_penalties + penalties(@assignment.id) +end + +# Helper to summarize reviews by reviewee +def fetch_feedback_summary + summary_ws_url = WEBSERVICE_CONFIG['summary_webservice_url'] + sum = SummaryHelper::Summary.new.summarize_reviews_by_reviewee(@questions, @assignment, @team_id, summary_ws_url, session) + @summary = sum.summary + @avg_scores_by_round = sum.avg_scores_by_round + @avg_scores_by_criterion = sum.avg_scores_by_criterion +end + +def process_questionare_for_team(assignment, team_id) + vmlist = [] + + counter_for_same_rubric = 0 + if @assignment.vary_by_topic? + topic_id = SignedUpTeam.topic_id_by_team_id(@team_id) + topic_specific_questionnaire = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: topic_id).first.questionnaire + @vmlist << populate_view_model(topic_specific_questionnaire) + end + + questionnaires.each do |questionnaire| + @round = nil + + # Guard clause to skip questionnaires that have already been populated for topic specific reviewing + if @assignment.vary_by_topic? && questionnaire.type == 'ReviewQuestionnaire' + next # Assignments with topic specific rubrics cannot have multiple rounds of review + end + + if @assignment.varying_rubrics_by_round? && questionnaire.type == 'ReviewQuestionnaire' + questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id) + if questionnaires.count > 1 + @round = questionnaires[counter_for_same_rubric].used_in_round + counter_for_same_rubric += 1 + else + @round = questionnaires[0].used_in_round + counter_for_same_rubric = 0 + end + end + vmlist << populate_view_model(questionnaire) + end + return vmlist +end + + +# Copied from Expertiza code +def redirect_when_disallowed + # For author feedback, participants need to be able to read feedback submitted by other teammates. + # If response is anything but author feedback, only the person who wrote feedback should be able to see it. + ## This following code was cloned from response_controller. + + # ACS Check if team count is more than 1 instead of checking if it is a team assignment + if @participant.assignment.max_team_size > 1 + team = @participant.team + unless team.nil? || (team.user? session[:user]) + flash[:error] = 'You are not on the team that wrote this feedback' + redirect_to '/' + return true + end + else + reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first + return true unless current_user_id?(reviewer.try(:user_id)) + end + false +end + end \ No newline at end of file diff --git a/app/helpers/penalty_helper.rb b/app/helpers/penalty_helper.rb index 9b974df20..c301984b2 100644 --- a/app/helpers/penalty_helper.rb +++ b/app/helpers/penalty_helper.rb @@ -1,3 +1,109 @@ module PenaltyHelper + def calculate_penalty(participant_id) + @submission_deadline_type_id = 1 + @review_deadline_type_id = 2 + @meta_review_deadline_type_id = 5 + @participant = AssignmentParticipant.find(participant_id) + @assignment = @participant.assignment + if @assignment.late_policy_id + late_policy = LatePolicy.find(@assignment.late_policy_id) + @penalty_per_unit = late_policy.penalty_per_unit + @max_penalty_for_no_submission = late_policy.max_penalty + @penalty_unit = late_policy.penalty_unit + end + penalties = { submission: 0, review: 0, meta_review: 0 } + penalties[:submission] = calculate_submission_penalty + penalties[:review] = calculate_review_penalty + penalties[:meta_review] = calculate_meta_review_penalty + penalties + end + def calculate_submission_penalty + return 0 if @penalty_per_unit.nil? + + submission_due_date = AssignmentDueDate.where(deadline_type_id: @submission_deadline_type_id, + parent_id: @assignment.id).first.due_at + submission_records = SubmissionRecord.where(team_id: @participant.team.id, assignment_id: @participant.assignment.id) + late_submission_times = submission_records.select { |submission_record| submission_record.updated_at > submission_due_date } + if late_submission_times.any? + last_submission_time = late_submission_times.last.updated_at + if last_submission_time > submission_due_date + time_difference = last_submission_time - submission_due_date + penalty_units = calculate_penalty_units(time_difference, @penalty_unit) + penalty_for_submission = penalty_units * @penalty_per_unit + if penalty_for_submission > @max_penalty_for_no_submission + @max_penalty_for_no_submission + else + penalty_for_submission + end + end + else + submission_records.any? ? 0 : @max_penalty_for_no_submission + end + end + + def calculate_review_penalty + penalty = 0 + num_of_reviews_required = @assignment.num_reviews + if num_of_reviews_required > 0 && !@penalty_per_unit.nil? + review_mappings = ReviewResponseMap.where(reviewer_id: @participant.get_reviewer.id) + review_due_date = AssignmentDueDate.where(deadline_type_id: @review_deadline_type_id, + parent_id: @assignment.id).first + penalty = compute_penalty_on_reviews(review_mappings, review_due_date.due_at, num_of_reviews_required) unless review_due_date.nil? + end + penalty + end + + def calculate_meta_review_penalty + penalty = 0 + num_of_meta_reviews_required = @assignment.num_review_of_reviews + if num_of_meta_reviews_required > 0 && !@penalty_per_unit.nil? + meta_review_mappings = MetareviewResponseMap.where(reviewer_id: @participant.id) + meta_review_due_date = AssignmentDueDate.where(deadline_type_id: @meta_review_deadline_type_id, + parent_id: @assignment.id).first + penalty = compute_penalty_on_reviews(meta_review_mappings, meta_review_due_date.due_at, num_of_meta_reviews_required) unless meta_review_due_date.nil? + end + penalty + end + + def compute_penalty_on_reviews(review_mappings, review_due_date, num_of_reviews_required) + review_map_created_at_list = [] + penalty = 0 + # Calculate the number of reviews that the user has completed so far. + review_mappings.each do |map| + unless map.response.empty? + created_at = Response.find_by(map_id: map.id).created_at + review_map_created_at_list << created_at + end + end + review_map_created_at_list.sort! + (0...num_of_reviews_required).each do |i| + if review_map_created_at_list.at(i) + if review_map_created_at_list.at(i) > review_due_date + time_difference = review_map_created_at_list.at(i) - review_due_date + penalty_units = calculate_penalty_units(time_difference, @penalty_unit) + penalty_for_this_review = penalty_units * @penalty_per_unit + if penalty_for_this_review > @max_penalty_for_no_submission + penalty = @max_penalty_for_no_submission + else + penalty += penalty_for_this_review + end + end + else + penalty = @max_penalty_for_no_submission + end + end + penalty + end + + def calculate_penalty_units(time_difference, penalty_unit) + case penalty_unit + when 'Minute' + time_difference / 60 + when 'Hour' + time_difference / 3600 + when 'Day' + time_difference / 86_400 + end + end end \ No newline at end of file diff --git a/app/models/response.rb b/app/models/response.rb index 9e07fd79d..5fd159c5d 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -56,4 +56,368 @@ def aggregate_questionnaire_score end sum end + + def aggregate_assessment_scores(assessments, questions) + scores = {} + if assessments.present? + scores[:max] = -999_999_999 + scores[:min] = 999_999_999 + total_score = 0 + length_of_assessments = assessments.length.to_f + assessments.each do |assessment| + curr_score = assessment_score(response: [assessment], questions: questions) + + scores[:max] = curr_score if curr_score > scores[:max] + scores[:min] = curr_score unless curr_score >= scores[:min] || curr_score == -1 + + # Check if the review is invalid. If is not valid do not include in score calculation + if curr_score == -1 + length_of_assessments -= 1 + curr_score = 0 + end + total_score += curr_score + end + scores[:avg] = if length_of_assessments.zero? + 0 + else + total_score.to_f / length_of_assessments + end + else + scores[:max] = nil + scores[:min] = nil + scores[:avg] = nil + end + scores + end + + # Computes the total score for an assessment + # params + # assessment - specifies the assessment for which the total score is being calculated + # questions - specifies the list of questions being evaluated in the assessment + # Called in: bookmarks_controller.rb (specific_average_score), grades_helper.rb (score_vector), response.rb (self.score), scoring.rb + def assessment_score(params) + @response = params[:response].last + return -1.0 if @response.nil? + + if @response + questions = params[:questions] + return -1.0 if questions.nil? + + weighted_score = 0 + sum_of_weights = 0 + @questionnaire = Questionnaire.find(questions.first.questionnaire_id) + + # Retrieve data for questionnaire (max score, sum of scores, weighted scores, etc.) + questionnaire_data = ScoreView.questionnaire_data(questions[0].questionnaire_id, @response.id) + weighted_score = questionnaire_data.weighted_score.to_f unless questionnaire_data.weighted_score.nil? + sum_of_weights = questionnaire_data.sum_of_weights.to_f + answers = Answer.where(response_id: @response.id) + answers.each do |answer| + question = Question.find(answer.question_id) + if answer.answer.nil? && question.is_a?(ScoredQuestion) + sum_of_weights -= Question.find(answer.question_id).weight + end + end + max_question_score = questionnaire_data.q1_max_question_score.to_f + if sum_of_weights > 0 && max_question_score && weighted_score > 0 + return (weighted_score / (sum_of_weights * max_question_score)) * 100 + else + return -1.0 # indicating no score + end + end + end + + # Compute total score for this assignment by summing the scores given on all questionnaires. + # Only scores passed in are included in this sum. + # Called in: scoring.rb + def compute_total_score(assignment, scores) + total = 0 + assignment.questionnaires.each { |questionnaire| total += questionnaire.get_weighted_score(assignment, scores) } + total + end + + # Computes and returns the scores of assignment for participants and teams + # Returns data in the format of + # { + # :particpant => { + # : => participant_scores(participant, questions), + # : => participant_scores(participant, questions) + # }, + # :teams => { + # :0 => {:team => team, + # :scores => assignment.vary_by_round? ? + # merge_grades_by_rounds(assignment, grades_by_rounds, total_num_of_assessments, total_score) + # : aggregate_assessment_scores(assessments, questions[:review]) + # } , + # :1 => {:team => team, + # :scores => assignment.vary_by_round? ? + # merge_grades_by_rounds(assignment, grades_by_rounds, total_num_of_assessments, total_score) + # : aggregate_assessment_scores(assessments, questions[:review]) + # } , + # } + # } + # Called in: grades_controller.rb (view), assignment.rb (self.export) + def review_grades(assignment, questions) + scores = { participants: {}, teams: {} } + assignment.participants.each do |participant| + scores[:participants][participant.id.to_s.to_sym] = participant_scores(participant, questions) + end + assignment.teams.each_with_index do |team, index| + scores[:teams][index.to_s.to_sym] = { team: team, scores: {} } + if assignment.varying_rubrics_by_round? + grades_by_rounds, total_num_of_assessments, total_score = compute_grades_by_rounds(assignment, questions, team) + # merge the grades from multiple rounds + scores[:teams][index.to_s.to_sym][:scores] = merge_grades_by_rounds(assignment, grades_by_rounds, total_num_of_assessments, total_score) + else + assessments = ReviewResponseMap.assessments_for(team) + scores[:teams][index.to_s.to_sym][:scores] = aggregate_assessment_scores(assessments, questions[:review]) + end + end + scores + end + + + def participant_scores(participant, questions) + assignment = participant.assignment + scores = {} + scores[:participant] = participant + compute_assignment_score(participant, questions, scores) + # Compute the Total Score (with question weights factored in) + scores[:total_score] = compute_total_score(assignment, scores) + + # merge scores[review#] (for each round) to score[review] + merge_scores(participant, scores) if assignment.varying_rubrics_by_round? + # In the event that this is a microtask, we need to scale the score accordingly and record the total possible points + if assignment.microtask? + topic = SignUpTopic.find_by(assignment_id: assignment.id) + return if topic.nil? + + scores[:total_score] *= (topic.micropayment.to_f / 100.to_f) + scores[:max_pts_available] = topic.micropayment + end + + scores[:total_score] = compute_total_score(assignment, scores) + + # update :total_score key in scores hash to user's current grade if they have one + # update :total_score key in scores hash to 100 if the current value is greater than 100 + if participant.grade + scores[:total_score] = participant.grade + else + scores[:total_score] = 100 if scores[:total_score] > 100 + end + scores + end + + # this function modifies the scores object passed to it from participant_grades + # this function should not be called in other contexts, since it is highly dependent on a specific scores structure, described above + # retrieves the symbol of eeach questionnaire associated with a participant on a given assignment + # returns all the associated reviews with a participant, indexed under :assessments + # returns the score assigned for the TOTAL body of responses associated with the user + # Called in: scoring.rb + def compute_assignment_score(participant, questions, scores) + participant.assignment.questionnaires.each do |questionnaire| + round = AssignmentQuestionnaire.find_by(assignment_id: participant.assignment.id, questionnaire_id: questionnaire.id).used_in_round + # create symbol for "varying rubrics" feature -Yang + questionnaire_symbol = if round.nil? + questionnaire.symbol + else + (questionnaire.symbol.to_s + round.to_s).to_sym + end + scores[questionnaire_symbol] = {} + + scores[questionnaire_symbol][:assessments] = if round.nil? + questionnaire.get_assessments_for(participant) + else + questionnaire.get_assessments_round_for(participant, round) + end + # aggregate_assessment_scores computes the total score for a list of responses to a questionnaire + scores[questionnaire_symbol][:scores] = aggregate_assessment_scores(scores[questionnaire_symbol][:assessments], questions[questionnaire_symbol]) + end + end + + # for each assignment review all scores and determine a max, min and average value + # this will be called when the assignment has various rounds, so we need to aggregate the scores across rounds + # achieves this by returning all the reviews, no longer delineated by round, and by returning the max, min and average + # Called in: scoring.rb + def merge_scores(participant, scores) + review_sym = 'review'.to_sym + scores[review_sym] = {} + scores[review_sym][:assessments] = [] + scores[review_sym][:scores] = { max: -999_999_999, min: 999_999_999, avg: 0 } + total_score = 0 + (1..participant.assignment.num_review_rounds).each do |i| + round_sym = ('review' + i.to_s).to_sym + # check if that assignment round is empty + next if scores[round_sym].nil? || scores[round_sym][:assessments].nil? || scores[round_sym][:assessments].empty? + + length_of_assessments = scores[round_sym][:assessments].length.to_f + scores[review_sym][:assessments] += scores[round_sym][:assessments] + + # update the max value if that rounds max exists and is higher than the current max + update_max_or_min(scores, round_sym, review_sym, :max) + # update the min value if that rounds min exists and is lower than the current min + update_max_or_min(scores, round_sym, review_sym, :min) + # Compute average score for current round, and sets overall total score to be average_from_round * length of assignment (# of questions) + total_score += scores[round_sym][:scores][:avg] * length_of_assessments unless scores[round_sym][:scores][:avg].nil? + end + # if the scores max and min weren't updated set them to zero. + if scores[review_sym][:scores][:max] == -999_999_999 && scores[review_sym][:scores][:min] == 999_999_999 + scores[review_sym][:scores][:max] = 0 + scores[review_sym][:scores][:min] = 0 + end + # Compute the average score for a particular review (all rounds) + scores[review_sym][:scores][:avg] = total_score / scores[review_sym][:assessments].length.to_f + end + + # Called in: scoring.rb + def update_max_or_min(scores, round_sym, review_sym, symbol) + op = :< if symbol == :max + op = :> if symbol == :min + # check if there is a max/min score for this particular round + unless scores[round_sym][:scores][symbol].nil? + # if scores[review_sym][:scores][symbol] (< or >) scores[round_sym][:scores][symbol] + if scores[review_sym][:scores][symbol].send(op, scores[round_sym][:scores][symbol]) + scores[review_sym][:scores][symbol] = scores[round_sym][:scores][symbol] + end + end + end + + # Called in: report_formatter_helper.rb (review_response_map) + def compute_reviews_hash(assignment) + review_scores = {} + response_type = 'ReviewResponseMap' + response_maps = ResponseMap.where(reviewed_object_id: assignment.id, type: response_type) + if assignment.varying_rubrics_by_round? + review_scores = scores_varying_rubrics(assignment, review_scores, response_maps) + else + review_scores = scores_non_varying_rubrics(assignment, review_scores, response_maps) + end + review_scores + end + + # calculate the avg score and score range for each reviewee(team), only for peer-review + # Called in: report_formatter_helper.rb (review_response_map) + def compute_avg_and_ranges_hash(assignment) + scores = {} + contributors = assignment.contributors # assignment_teams + if assignment.varying_rubrics_by_round? + rounds = assignment.rounds_of_reviews + (1..rounds).each do |round| + contributors.each do |contributor| + questions = peer_review_questions_for_team(assignment, contributor, round) + assessments = ReviewResponseMap.assessments_for(contributor) + assessments.select! { |assessment| assessment.round == round } + scores[contributor.id] = {} if round == 1 + scores[contributor.id][round] = {} + scores[contributor.id][round] = aggregate_assessment_scores(assessments, questions) + end + end + else + contributors.each do |contributor| + questions = peer_review_questions_for_team(assignment, contributor) + assessments = ReviewResponseMap.assessments_for(contributor) + scores[contributor.id] = {} + scores[contributor.id] = aggregate_assessment_scores(assessments, questions) + end + end + scores + end +end + +private + +# Get all of the questions asked during peer review for the given team's work +def peer_review_questions_for_team(assignment, team, round_number = nil) + return nil if team.nil? + + signed_up_team = SignedUpTeam.find_by(team_id: team.id) + topic_id = signed_up_team.topic_id unless signed_up_team.nil? + review_questionnaire_id = assignment.review_questionnaire_id(round_number, topic_id) unless team.nil? + Question.where(questionnaire_id: review_questionnaire_id).to_a unless team.nil? +end + +def calc_review_score(corresponding_response, questions) + if corresponding_response.empty? + return -1.0 + else + this_review_score_raw = assessment_score(response: corresponding_response, questions: questions) + if this_review_score_raw + return ((this_review_score_raw * 100) / 100.0).round if this_review_score_raw >= 0.0 + end + end +end + +def scores_varying_rubrics(assignment, review_scores, response_maps) + rounds = assignment.rounds_of_reviews + (1..rounds).each do |round| + response_maps.each do |response_map| + questions = peer_review_questions_for_team(assignment, response_map.reviewee, round) + reviewer = review_scores[response_map.reviewer_id] + corresponding_response = Response.where('map_id = ?', response_map.id) + corresponding_response = corresponding_response.select { |response| response.round == round } unless corresponding_response.empty? + respective_scores = {} + respective_scores = reviewer[round] unless reviewer.nil? || reviewer[round].nil? + this_review_score = calc_review_score(corresponding_response, questions) + review_scores[response_map.reviewer_id] = {} unless review_scores[response_map.reviewer_id] + respective_scores[response_map.reviewee_id] = this_review_score + review_scores[response_map.reviewer_id][round] = respective_scores + reviewer = {} if reviewer.nil? + reviewer[round] = {} if reviewer[round].nil? + reviewer[round] = respective_scores + end + end + review_scores +end + +def scores_non_varying_rubrics(assignment, review_scores, response_maps) + response_maps.each do |response_map| + questions = peer_review_questions_for_team(assignment, response_map.reviewee) + reviewer = review_scores[response_map.reviewer_id] + corresponding_response = Response.where('map_id = ?', response_map.id) + respective_scores = {} + respective_scores = reviewer unless reviewer.nil? + this_review_score = calc_review_score(corresponding_response, questions) + respective_scores[response_map.reviewee_id] = this_review_score + review_scores[response_map.reviewer_id] = respective_scores + end + review_scores +end + +# Below private methods are extracted and added as part of refactoring project E2009 - Spring 2020 +# This method computes and returns grades by rounds, total_num_of_assessments and total_score +# when the assignment has varying rubrics by round +def compute_grades_by_rounds(assignment, questions, team) + grades_by_rounds = {} + total_score = 0 + total_num_of_assessments = 0 # calculate grades for each rounds + (1..assignment.num_review_rounds).each do |i| + assessments = ReviewResponseMap.get_responses_for_team_round(team, i) + round_sym = ('review' + i.to_s).to_sym + grades_by_rounds[round_sym] = aggregate_assessment_scores(assessments, questions[round_sym]) + total_num_of_assessments += assessments.size + total_score += grades_by_rounds[round_sym][:avg] * assessments.size.to_f unless grades_by_rounds[round_sym][:avg].nil? + end + [grades_by_rounds, total_num_of_assessments, total_score] +end + +# merge the grades from multiple rounds +def merge_grades_by_rounds(assignment, grades_by_rounds, num_of_assessments, total_score) + team_scores = { max: 0, min: 0, avg: nil } + return team_scores if num_of_assessments.zero? + + team_scores[:max] = -999_999_999 + team_scores[:min] = 999_999_999 + team_scores[:avg] = total_score / num_of_assessments + (1..assignment.num_review_rounds).each do |i| + round_sym = ('review' + i.to_s).to_sym + unless grades_by_rounds[round_sym][:max].nil? || team_scores[:max] >= grades_by_rounds[round_sym][:max] + team_scores[:max] = grades_by_rounds[round_sym][:max] + end + unless grades_by_rounds[round_sym][:min].nil? || team_scores[:min] <= grades_by_rounds[round_sym][:min] + team_scores[:min] = grades_by_rounds[round_sym][:min] + end + end + team_scores +end + end From be289cf263595685cf2f23735c14e8762a1be04f Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Mon, 17 Mar 2025 16:20:33 -0400 Subject: [PATCH 13/41] Added the populate_view_model --- app/controllers/api/v1/grades_controller.rb | 8 +- app/helpers/grades_helper.rb | 161 +++++++++++--------- 2 files changed, 91 insertions(+), 78 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 3dde2f5a8..178d382b5 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -20,8 +20,9 @@ def view fetch_penalties @show_reputation = false - + + def view_my_scores fetch_participant_and_assignment @team_id = TeamsUser.team_id(@participant.parent_id, @participant.user_id) @@ -218,9 +219,8 @@ def find_assignment(assignment_id) end -# Helper methods for views - - + # Helper methods for views + def filter_questionnaires(assignment) questionnaires = assignment.questionnaires if assignment.varying_rubrics_by_round? diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index b923fae3c..eb4350620 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -24,89 +24,102 @@ def penalties(assignment_id) # Helper to retrieve participant and related assignment data -def fetch_participant_and_assignment - @participant = AssignmentParticipant.find(params[:id]) - @assignment = @participant.assignment -end - -# Helper to retrieve questionnaires and questions -def fetch_questionnaires_and_questions - questionnaires = @assignment.questionnaires - @questions = retrieve_questions(questionnaires, @assignment.id) -end - -# Helper to fetch participant scores -def fetch_participant_scores - @pscore = participant_scores(@participant, @questions) -end - -# Helper to calculate penalties -def fetch_penalties - penalties(@assignment.id) -end - -# Helper to summarize reviews by reviewee -def fetch_feedback_summary - summary_ws_url = WEBSERVICE_CONFIG['summary_webservice_url'] - sum = SummaryHelper::Summary.new.summarize_reviews_by_reviewee(@questions, @assignment, @team_id, summary_ws_url, session) - @summary = sum.summary - @avg_scores_by_round = sum.avg_scores_by_round - @avg_scores_by_criterion = sum.avg_scores_by_criterion -end - -def process_questionare_for_team(assignment, team_id) - vmlist = [] - - counter_for_same_rubric = 0 - if @assignment.vary_by_topic? - topic_id = SignedUpTeam.topic_id_by_team_id(@team_id) - topic_specific_questionnaire = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: topic_id).first.questionnaire - @vmlist << populate_view_model(topic_specific_questionnaire) + def fetch_participant_and_assignment + @participant = AssignmentParticipant.find(params[:id]) + @assignment = @participant.assignment end - questionnaires.each do |questionnaire| - @round = nil + # Helper to retrieve questionnaires and questions + def fetch_questionnaires_and_questions + questionnaires = @assignment.questionnaires + @questions = retrieve_questions(questionnaires, @assignment.id) + end + + # Helper to fetch participant scores + def fetch_participant_scores + @pscore = participant_scores(@participant, @questions) + end + + # Helper to calculate penalties + def fetch_penalties + penalties(@assignment.id) + end - # Guard clause to skip questionnaires that have already been populated for topic specific reviewing - if @assignment.vary_by_topic? && questionnaire.type == 'ReviewQuestionnaire' - next # Assignments with topic specific rubrics cannot have multiple rounds of review + # Helper to summarize reviews by reviewee + def fetch_feedback_summary + summary_ws_url = WEBSERVICE_CONFIG['summary_webservice_url'] + sum = SummaryHelper::Summary.new.summarize_reviews_by_reviewee(@questions, @assignment, @team_id, summary_ws_url, session) + @summary = sum.summary + @avg_scores_by_round = sum.avg_scores_by_round + @avg_scores_by_criterion = sum.avg_scores_by_criterion + end + + def process_questionare_for_team(assignment, team_id) + vmlist = [] + + counter_for_same_rubric = 0 + if @assignment.vary_by_topic? + topic_id = SignedUpTeam.topic_id_by_team_id(@team_id) + topic_specific_questionnaire = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: topic_id).first.questionnaire + @vmlist << populate_view_model(topic_specific_questionnaire) end - if @assignment.varying_rubrics_by_round? && questionnaire.type == 'ReviewQuestionnaire' - questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id) - if questionnaires.count > 1 - @round = questionnaires[counter_for_same_rubric].used_in_round - counter_for_same_rubric += 1 - else - @round = questionnaires[0].used_in_round - counter_for_same_rubric = 0 + questionnaires.each do |questionnaire| + @round = nil + + # Guard clause to skip questionnaires that have already been populated for topic specific reviewing + if @assignment.vary_by_topic? && questionnaire.type == 'ReviewQuestionnaire' + next # Assignments with topic specific rubrics cannot have multiple rounds of review end + + if @assignment.varying_rubrics_by_round? && questionnaire.type == 'ReviewQuestionnaire' + questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id) + if questionnaires.count > 1 + @round = questionnaires[counter_for_same_rubric].used_in_round + counter_for_same_rubric += 1 + else + @round = questionnaires[0].used_in_round + counter_for_same_rubric = 0 + end + end + vmlist << populate_view_model(questionnaire) end - vmlist << populate_view_model(questionnaire) + return vmlist end - return vmlist -end - - -# Copied from Expertiza code -def redirect_when_disallowed - # For author feedback, participants need to be able to read feedback submitted by other teammates. - # If response is anything but author feedback, only the person who wrote feedback should be able to see it. - ## This following code was cloned from response_controller. - - # ACS Check if team count is more than 1 instead of checking if it is a team assignment - if @participant.assignment.max_team_size > 1 - team = @participant.team - unless team.nil? || (team.user? session[:user]) - flash[:error] = 'You are not on the team that wrote this feedback' - redirect_to '/' - return true + + + + + # Copied from Expertiza code + def redirect_when_disallowed + # For author feedback, participants need to be able to read feedback submitted by other teammates. + # If response is anything but author feedback, only the person who wrote feedback should be able to see it. + ## This following code was cloned from response_controller. + + # ACS Check if team count is more than 1 instead of checking if it is a team assignment + if @participant.assignment.max_team_size > 1 + team = @participant.team + unless team.nil? || (team.user? session[:user]) + flash[:error] = 'You are not on the team that wrote this feedback' + redirect_to '/' + return true + end + else + reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first + return true unless current_user_id?(reviewer.try(:user_id)) end - else - reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first - return true unless current_user_id?(reviewer.try(:user_id)) + false + end + + def populate_view_model(questionnaire) + vm = VmQuestionResponse.new(questionnaire, @assignment, @round) + vmquestions = questionnaire.questions + vm.add_questions(vmquestions) + vm.add_team_members(@team) + qn = AssignmentQuestionnaire.where(assignment_id: @assignment.id, used_in_round: 2).size >= 1 + vm.add_reviews(@participant, @team, @assignment.varying_rubrics_by_round?) + vm.calculate_metrics + vm end - false -end end \ No newline at end of file From 8fa3f7101a888e97a8477c938dcc0f837840e11e Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 17 Mar 2025 23:28:23 -0400 Subject: [PATCH 14/41] Refactored unsubmitted_self_review and Penalty Helper --- app/controllers/api/v1/grades_controller.rb | 2 +- app/helpers/grades_helper.rb | 40 +++-- app/helpers/penalty_helper.rb | 164 ++++++++++++-------- app/models/assignment_due_date.rb | 3 + app/models/metareview_response_map.rb | 7 + app/models/response_map.rb | 19 ++- 6 files changed, 149 insertions(+), 86 deletions(-) create mode 100644 app/models/metareview_response_map.rb diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 178d382b5..67a496612 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -152,7 +152,7 @@ def self_review_finished? participant = Participant.find(params[:id]) assignment = participant.try(:assignment) self_review_enabled = assignment.try(:is_selfreview_enabled) - not_submitted = ResponseMap.unsubmitted_self_review?(participant.try(:id)) + not_submitted = ResponseMap.self_review_pending?(participant.try(:id)) if self_review_enabled !not_submitted else diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index eb4350620..fe728c9c5 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -1,25 +1,43 @@ module GradesHelper include PenaltyHelper - # This function calculates all the penalties def penalties(assignment_id) - @all_penalties = {} @assignment = Assignment.find(assignment_id) - calculate_for_participants = true unless @assignment.is_penalty_calculated + calculate_for_participants = should_calculate_penalties? + Participant.where(parent_id: assignment_id).each do |participant| penalties = calculate_penalty(participant.id) - @total_penalty = 0 - - unless penalties[:submission].zero? || penalties[:review].zero? || penalties[:meta_review].zero? - - @total_penalty = (penalties[:submission] + penalties[:review] + penalties[:meta_review]) - l_policy = LatePolicy.find(@assignment.late_policy_id) - @total_penalty = l_policy.max_penalty if @total_penalty > l_policy.max_penalty + @total_penalty = calculate_total_penalty(penalties) + + if @total_penalty > 0 + @total_penalty = apply_max_penalty(@total_penalty) attributes(@participant) if calculate_for_participants end + assign_all_penalties(participant, penalties) end - @assignment[:is_penalty_calculated] = true unless @assignment.is_penalty_calculated + + mark_penalty_as_calculated unless @assignment.is_penalty_calculated + end + + private + + def should_calculate_penalties? + !@assignment.is_penalty_calculated + end + + def calculate_total_penalty(penalties) + total = penalties[:submission] + penalties[:review] + penalties[:meta_review] + total > 0 ? total : 0 + end + + def apply_max_penalty(total_penalty) + late_policy = LatePolicy.find(@assignment.late_policy_id) + total_penalty > late_policy.max_penalty ? late_policy.max_penalty : total_penalty + end + + def mark_penalty_as_calculated + @assignment.update(is_penalty_calculated: true) end diff --git a/app/helpers/penalty_helper.rb b/app/helpers/penalty_helper.rb index c301984b2..7067f3f82 100644 --- a/app/helpers/penalty_helper.rb +++ b/app/helpers/penalty_helper.rb @@ -1,100 +1,126 @@ module PenaltyHelper def calculate_penalty(participant_id) - @submission_deadline_type_id = 1 - @review_deadline_type_id = 2 - @meta_review_deadline_type_id = 5 - @participant = AssignmentParticipant.find(participant_id) - @assignment = @participant.assignment - if @assignment.late_policy_id - late_policy = LatePolicy.find(@assignment.late_policy_id) - @penalty_per_unit = late_policy.penalty_per_unit - @max_penalty_for_no_submission = late_policy.max_penalty - @penalty_unit = late_policy.penalty_unit - end + set_participant_and_assignment(participant_id) + set_late_policy if @assignment.late_policy_id + penalties = { submission: 0, review: 0, meta_review: 0 } penalties[:submission] = calculate_submission_penalty penalties[:review] = calculate_review_penalty penalties[:meta_review] = calculate_meta_review_penalty penalties end + + def set_participant_and_assignment(participant_id) + @participant = AssignmentParticipant.find(participant_id) + @assignment = @participant.assignment + end + + def set_late_policy + late_policy = LatePolicy.find(@assignment.late_policy_id) + @penalty_per_unit = late_policy.penalty_per_unit + @max_penalty_for_no_submission = late_policy.max_penalty + @penalty_unit = late_policy.penalty_unit + end def calculate_submission_penalty return 0 if @penalty_per_unit.nil? - - submission_due_date = AssignmentDueDate.where(deadline_type_id: @submission_deadline_type_id, - parent_id: @assignment.id).first.due_at + + submission_due_date = get_submission_due_date submission_records = SubmissionRecord.where(team_id: @participant.team.id, assignment_id: @participant.assignment.id) - late_submission_times = submission_records.select { |submission_record| submission_record.updated_at > submission_due_date } + late_submission_times = get_late_submission_times(submission_records, submission_due_date) + if late_submission_times.any? - last_submission_time = late_submission_times.last.updated_at - if last_submission_time > submission_due_date - time_difference = last_submission_time - submission_due_date - penalty_units = calculate_penalty_units(time_difference, @penalty_unit) - penalty_for_submission = penalty_units * @penalty_per_unit - if penalty_for_submission > @max_penalty_for_no_submission - @max_penalty_for_no_submission - else - penalty_for_submission - end - end + calculate_late_submission_penalty(late_submission_times.last.updated_at, submission_due_date) else - submission_records.any? ? 0 : @max_penalty_for_no_submission + handle_no_submission(submission_records) end end - - def calculate_review_penalty - penalty = 0 - num_of_reviews_required = @assignment.num_reviews - if num_of_reviews_required > 0 && !@penalty_per_unit.nil? - review_mappings = ReviewResponseMap.where(reviewer_id: @participant.get_reviewer.id) - review_due_date = AssignmentDueDate.where(deadline_type_id: @review_deadline_type_id, - parent_id: @assignment.id).first - penalty = compute_penalty_on_reviews(review_mappings, review_due_date.due_at, num_of_reviews_required) unless review_due_date.nil? + + def get_submission_due_date + AssignmentDueDate.where(deadline_type_id: @submission_deadline_type_id, parent_id: @assignment.id).first.due_at + end + + def get_late_submission_times(submission_records, submission_due_date) + submission_records.select { |submission_record| submission_record.updated_at > submission_due_date } + end + + def calculate_late_submission_penalty(last_submission_time, submission_due_date) + return 0 if last_submission_time <= submission_due_date + + time_difference = last_submission_time - submission_due_date + penalty_units = calculate_penalty_units(time_difference, @penalty_unit) + penalty_for_submission = penalty_units * @penalty_per_unit + apply_max_penalty_limit(penalty_for_submission) + end + + def handle_no_submission(submission_records) + submission_records.any? ? 0 : @max_penalty_for_no_submission + end + + def apply_max_penalty_limit(penalty_for_submission) + if penalty_for_submission > @max_penalty_for_no_submission + @max_penalty_for_no_submission + else + penalty_for_submission end - penalty end + def calculate_review_penalty + calculate_penalty(@assignment.num_reviews, @review_deadline_type_id, ReviewResponseMap, :get_reviewer) + end + def calculate_meta_review_penalty - penalty = 0 - num_of_meta_reviews_required = @assignment.num_review_of_reviews - if num_of_meta_reviews_required > 0 && !@penalty_per_unit.nil? - meta_review_mappings = MetareviewResponseMap.where(reviewer_id: @participant.id) - meta_review_due_date = AssignmentDueDate.where(deadline_type_id: @meta_review_deadline_type_id, - parent_id: @assignment.id).first - penalty = compute_penalty_on_reviews(meta_review_mappings, meta_review_due_date.due_at, num_of_meta_reviews_required) unless meta_review_due_date.nil? - end - penalty + calculate_penalty(@assignment.num_review_of_reviews, @meta_review_deadline_type_id, MetareviewResponseMap, :id) + end + + private + + def calculate_penalty(num_reviews_required, deadline_type_id, mapping_class, reviewer_method) + return 0 if num_reviews_required <= 0 || @penalty_per_unit.nil? + + review_mappings = mapping_class.where(reviewer_id: @participant.send(reviewer_method).id) + review_due_date = AssignmentDueDate.where(deadline_type_id: deadline_type_id, parent_id: @assignment.id).first + return 0 if review_due_date.nil? + + compute_penalty_on_reviews(review_mappings, review_due_date.due_at, num_reviews_required) end - def compute_penalty_on_reviews(review_mappings, review_due_date, num_of_reviews_required) - review_map_created_at_list = [] + def compute_penalty_on_reviews(review_mappings, review_due_date, num_of_reviews_required, penalty_unit, penalty_per_unit, max_penalty) + review_timestamps = collect_review_timestamps(review_mappings) + review_timestamps.sort! + penalty = 0 - # Calculate the number of reviews that the user has completed so far. - review_mappings.each do |map| - unless map.response.empty? - created_at = Response.find_by(map_id: map.id).created_at - review_map_created_at_list << created_at - end - end - review_map_created_at_list.sort! - (0...num_of_reviews_required).each do |i| - if review_map_created_at_list.at(i) - if review_map_created_at_list.at(i) > review_due_date - time_difference = review_map_created_at_list.at(i) - review_due_date - penalty_units = calculate_penalty_units(time_difference, @penalty_unit) - penalty_for_this_review = penalty_units * @penalty_per_unit - if penalty_for_this_review > @max_penalty_for_no_submission - penalty = @max_penalty_for_no_submission - else - penalty += penalty_for_this_review - end - end + + num_of_reviews_required.times do |i| + if review_timestamps[i] + penalty += calculate_review_penalty(review_timestamps[i], review_due_date, penalty_unit, penalty_per_unit, max_penalty) else - penalty = @max_penalty_for_no_submission + penalty = apply_max_penalty_if_missing(max_penalty) end end + penalty end + + private + + def collect_review_timestamps(review_mappings) + review_mappings.filter_map do |map| + Response.find_by(map_id: map.id)&.created_at unless map.response.empty? + end + end + + def calculate_review_penalty(submission_date, due_date, penalty_unit, penalty_per_unit, max_penalty) + return 0 if submission_date <= due_date + + time_difference = submission_date - due_date + penalty_units = calculate_penalty_units(time_difference, penalty_unit) + [penalty_units * penalty_per_unit, max_penalty].min + end + + def apply_max_penalty_if_missing(max_penalty) + max_penalty + end def calculate_penalty_units(time_difference, penalty_unit) case penalty_unit diff --git a/app/models/assignment_due_date.rb b/app/models/assignment_due_date.rb index 6246452be..736f972f2 100644 --- a/app/models/assignment_due_date.rb +++ b/app/models/assignment_due_date.rb @@ -1,3 +1,6 @@ class AssignmentDueDate < DueDate # Needed for backwards compatibility due to DueDate.type inheritance column + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'parent_id' + belongs_to :deadline_type, class_name: 'DeadlineType', foreign_key: 'deadline_type_id' + end diff --git a/app/models/metareview_response_map.rb b/app/models/metareview_response_map.rb new file mode 100644 index 000000000..e38fa641b --- /dev/null +++ b/app/models/metareview_response_map.rb @@ -0,0 +1,7 @@ +class MetareviewResponseMap < ResponseMap + belongs_to :reviewee, class_name: 'Participant', foreign_key: 'reviewee_id' + belongs_to :review_mapping, class_name: 'ResponseMap', foreign_key: 'reviewed_object_id' + delegate :assignment, to: :reviewee + +end + \ No newline at end of file diff --git a/app/models/response_map.rb b/app/models/response_map.rb index d1e88e74b..4b0db2618 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -39,11 +39,20 @@ def self.assessments_for(team) responses end - def unsubmitted_self_review?(participant_id) - self_review = SelfReviewResponseMap.where(reviewer_id: participant_id).first.try(:response).try(:last) - return !self_review.try(:is_submitted) if self_review - - true + def self_review_pending?(participant_id) + self_review = latest_self_review(participant_id) + return true if self_review.nil? + + !self_review.is_submitted + end + + private + + def latest_self_review(participant_id) + SelfReviewResponseMap.where(reviewer_id: participant_id) + .first + &.response + &.last end end From d30b2f1143a04726272e32ea51496e5cf1e55c39 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 17 Mar 2025 23:42:01 -0400 Subject: [PATCH 15/41] Refactored redirect_when_disallowed --- app/helpers/grades_helper.rb | 42 +++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index fe728c9c5..67091b6b9 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -106,28 +106,36 @@ def process_questionare_for_team(assignment, team_id) end - - - # Copied from Expertiza code def redirect_when_disallowed - # For author feedback, participants need to be able to read feedback submitted by other teammates. - # If response is anything but author feedback, only the person who wrote feedback should be able to see it. - ## This following code was cloned from response_controller. - - # ACS Check if team count is more than 1 instead of checking if it is a team assignment - if @participant.assignment.max_team_size > 1 - team = @participant.team - unless team.nil? || (team.user? session[:user]) - flash[:error] = 'You are not on the team that wrote this feedback' - redirect_to '/' - return true - end + if team_assignment? + redirect_if_not_on_correct_team else - reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first - return true unless current_user_id?(reviewer.try(:user_id)) + redirect_if_not_authorized_reviewer end false end + + private + + def team_assignment? + @participant.assignment.max_team_size > 1 + end + + def redirect_if_not_on_correct_team + team = @participant.team + if team.nil? || !team.user?(session[:user]) + flash[:error] = 'You are not on the team that wrote this feedback' + redirect_to '/' + end + end + + def redirect_if_not_authorized_reviewer + reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first + return if current_user_id?(reviewer.try(:user_id)) + + flash[:error] = 'You are not authorized to view this feedback' + redirect_to '/' + end def populate_view_model(questionnaire) vm = VmQuestionResponse.new(questionnaire, @assignment, @round) From 4b6b36822502545dbdb84c3f9eaaded1fc4b1eae Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Tue, 18 Mar 2025 13:57:43 -0400 Subject: [PATCH 16/41] Renamed edit and update functions --- app/controllers/api/v1/grades_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 67a496612..e48757398 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -59,7 +59,7 @@ def view_team - def edit + def edit_participant_scores @participant = find_participant(params[:id]) return unless @participant # Exit early if participant not found @assignment = @participant.assignment @@ -81,7 +81,7 @@ def list_questions(assignment) - def update + def update_participant_grade participant = AssignmentParticipant.find_by(id: params[:id]) return handle_not_found unless participant From 239abfec0c0edd08b98913dc1cdf1b8278fbe7e0 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Wed, 19 Mar 2025 22:51:16 -0400 Subject: [PATCH 17/41] Added end to view function --- app/controllers/api/v1/grades_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index e48757398..6da29c260 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -19,7 +19,7 @@ def view get_data_for_heat_map(params[:id]) fetch_penalties @show_reputation = false - + end From ed638edb60c32f4f1c98cc03ed68522c2d50fb55 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Wed, 19 Mar 2025 22:54:24 -0400 Subject: [PATCH 18/41] Renamed view function --- app/controllers/api/v1/grades_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 6da29c260..aa8d12811 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -15,7 +15,7 @@ def action_allowed? render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end - def view + def view_grading_report get_data_for_heat_map(params[:id]) fetch_penalties @show_reputation = false From 5ff4ea85ea1d3801d88a5c5df6d4a1b72b2cf302 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 11:16:45 -0400 Subject: [PATCH 19/41] Updated action-allowed for OCP, removed AuthorizationHelper --- app/controllers/api/v1/grades_controller.rb | 42 ++++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index aa8d12811..7a6ad35bb 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,20 +1,32 @@ class Api::V1::GradesController < ApplicationController - include AuthorizationHelper include GradesHelper - def action_allowed? - permitted = case params[:action] - when 'view_my_scores' - student_with_permissions? - when 'view_team' - student_viewing_own_team? || has_privileges_of?('Teaching Assistant') - else - has_privileges_of?('Teaching Assistant') - end + # def action_allowed? + # permitted = case params[:action] + # when 'view_my_scores' + # student_with_permissions? + # when 'view_team' + # student_viewing_own_team? || has_privileges_of?('Teaching Assistant') + # else + # has_privileges_of?('Teaching Assistant') + # end + # render json: { allowed: permitted }, status: permitted ? :ok : :forbidden + # end + + ACTION_PERMISSIONS = { + 'view_my_scores' => :student_with_permissions?, + 'view_team' => :student_or_ta? + }.freeze + + def action_allowed? + permitted = check_permission(params[:action]) render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end + + + def view_grading_report get_data_for_heat_map(params[:id]) fetch_penalties @@ -135,12 +147,22 @@ def instructor_review # Helper methods for action_allowed? + def check_permission(action) + return has_privileges_of?('Teaching Assistant') unless ACTION_PERMISSIONS.key?(action) + + send(ACTION_PERMISSIONS[action]) + end + def student_with_permissions? has_role?('Student') && self_review_finished? && are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') end + def student_or_ta? + student_viewing_own_team? || has_privileges_of?('Teaching Assistant') + end + def student_viewing_own_team? return false unless has_role?('Student') From 2f12c0ecaca43ba6bf15c131b57acdb9a6f54fbf Mon Sep 17 00:00:00 2001 From: cdandre5 Date: Sat, 22 Mar 2025 13:37:17 -0400 Subject: [PATCH 20/41] Wrote all of the comments for the methods and private methods in grades_controller and grades_helper --- app/controllers/api/v1/grades_controller.rb | 194 ++++++++++---------- app/helpers/grades_helper.rb | 119 ++++++++---- 2 files changed, 183 insertions(+), 130 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 7a6ad35bb..d380e7dc1 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -14,27 +14,38 @@ class Api::V1::GradesController < ApplicationController # render json: { allowed: permitted }, status: permitted ? :ok : :forbidden # end + + # Defines permissions for different actions based on user roles. + # 'view_my_scores' is allowed for students with specific permissions. + # 'view_team' is allowed for both students and TAs. + # 'view_grading_report' is allowed TAs and higher roles. ACTION_PERMISSIONS = { 'view_my_scores' => :student_with_permissions?, 'view_team' => :student_or_ta? }.freeze + # Determines if the current user is allowed to perform the specified action. + # Checks the permission using the action parameter and returns a JSON response. + # If the action is permitted, the response status is :ok; otherwise, it is :forbidden. def action_allowed? permitted = check_permission(params[:action]) render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end - - - + # The view_grading_report offers instructors a comprehensive overview of all grades for an assignment. + # It displays all participants and the reviews they have received. + # Additionally, it provides a final score, which is the average of all reviews, and highlights the greatest + # difference in scores among the reviews. def view_grading_report get_data_for_heat_map(params[:id]) fetch_penalties @show_reputation = false end - - + # The view_my_scores method provides participants with a detailed overview of their performance in an assignment. + # It retrieves and their questions and calculated scores and prepares feedback summaries. + # Additionally, it applies any penalties and determines the current stage of the assignment. + # This method ensures participants have a comprehensive understanding of their scores and feedback def view_my_scores fetch_participant_and_assignment @team_id = TeamsUser.team_id(@participant.parent_id, @participant.user_id) @@ -42,8 +53,7 @@ def view_my_scores fetch_questionnaires_and_questions fetch_participant_scores - # get_data_for_heat_map() - + @topic_id = SignedUpTeam.topic_id(@participant.assignment.id, @participant.user_id) @stage = @participant.assignment.current_stage(@topic_id) fetch_penalties @@ -52,9 +62,10 @@ def view_my_scores fetch_feedback_summary end - - - + # The view_team method provides an alternative view for participants, focusing on team performance. + # It retrieves the participant, assignment, and team information, and calculated scores and penalties. + # Additionally, it prepares the necessary data for displaying team-related information. + # This method ensures participants have a clear understanding of their team's performance and any associated penalties. def view_team fetch_participant_and_assignment @team = @participant.team @@ -70,7 +81,11 @@ def view_team end - + # Prepares the necessary information for editing grade details, including the participant, questions, scores, and assignment. + # The participant refers to the student whose grade is being modified. + # The assignment is the specific task for which the participant's grade is being reviewed. + # The questions are the rubric items or criteria associated with the assignment. + # The scores represent the combined scoring information for both the participant and their team, required for frontend display. def edit_participant_scores @participant = find_participant(params[:id]) return unless @participant # Exit early if participant not found @@ -79,20 +94,12 @@ def edit_participant_scores @scores = Response.review_grades(@participant, @questions) end - - - - - def list_questions(assignment) - assignment.questionnaires.each_with_object({}) do |questionnaire, questions| - questions[questionnaire.id.to_s] = questionnaire.questions - end - end - - - - - + + # Update method for the grade associated with a participant. + # Allows an instructor to upgrade a participant's grade and provide feedback on their assignment submission. + # The updated participant information is saved for future scoring evaluations. + # If saving the participant fails, a flash error populates. + # Finally, the instructor is redirected to the edit pages. def update_participant_grade participant = AssignmentParticipant.find_by(id: params[:id]) return handle_not_found unless participant @@ -101,15 +108,17 @@ def update_participant_grade if grade_changed?(participant, new_grade) participant.update(grade: new_grade) flash[:note] = grade_message(participant) + else + flash[:error] = 'Error updating participant grade.' end + # Redirect to the edit action for the participant. redirect_to action: 'edit', id: params[:id] end - - - - - + # Update the grade and comment for a participant's submission. + # Save the updated information for future evaluations. + # Handle errors by returning a bad_request response. + # Provide feedback to the user about the operation's success or failure. def update_team participant = AssignmentParticipant.find_by(id: params[:participant_id]) return handle_not_found unless participant @@ -123,11 +132,11 @@ def update_team redirect_to controller: 'grades', action: 'view_team', id: params[:id] end - - - - - + # Determines the appropriate controller action for an instructor's review based on the current state. + # This method checks if a review mapping exists for the participant. If it does, the instructor is directed to edit the existing review. + # If no review mapping exists, the instructor is directed to create a new review. + # The Response controller handles both creating and editing reviews through its response#new and response#edit actions. + # This method ensures the correct action is taken by checking the existence of a review mapping and utilizing the new_record functionality. def instructor_review participant = find_participant(params[:id]) return unless participant # Exit early if participant not found @@ -139,30 +148,29 @@ def instructor_review end - - private - - -# Helper methods for action_allowed? - + # Private Methods associated with action_allowed?: + # Checks if the user has permission for the given action and executes the corresponding method. def check_permission(action) return has_privileges_of?('Teaching Assistant') unless ACTION_PERMISSIONS.key?(action) send(ACTION_PERMISSIONS[action]) end + # Checks if the student has the necessary permissions and authorizations to proceed. def student_with_permissions? has_role?('Student') && self_review_finished? && are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') end + # Checks if the user is either a student viewing their own team or has Teaching Assistant privileges. def student_or_ta? student_viewing_own_team? || has_privileges_of?('Teaching Assistant') end + # This method checks if the current user, who must have the 'Student' role, is viewing their own team. def student_viewing_own_team? return false unless has_role?('Student') @@ -170,6 +178,7 @@ def student_viewing_own_team? participant && current_user_is_assignment_participant?(participant.assignment.id) end + # Check if the self-review for the participant is finished based on assignment settings and submission status. def self_review_finished? participant = Participant.find(params[:id]) assignment = participant.try(:assignment) @@ -183,89 +192,88 @@ def self_review_finished? end -# Helper methods for the instructor_review - - def find_or_create_reviewer(user_id, assignment_id) - reviewer = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) - reviewer.set_handle if reviewer.new_record? - reviewer + # Private Methods associated with View methods: + # Determines if the rubric changes by round and returns the corresponding questions based on the criteria. + def filter_questionnaires(assignment) + questionnaires = assignment.questionnaires + if assignment.varying_rubrics_by_round? + retrieve_questions(questionnaires, assignment.id) + else + questions = {} + questionnaires.each do |questionnaire| + questions[questionnaire.id.to_s.to_sym] = questionnaire.questions + end + questions + end end - def find_or_create_review_mapping(reviewee_id, reviewer_id, assignment_id) - ReviewResponseMap.find_or_create_by(reviewee_id: reviewee_id, reviewer_id: reviewer_id, reviewed_object_id: assignment_id) + # Generates data for visualizing heat maps in the view statements. + def get_data_for_heat_map(assignment_id) + # Finds the assignment + @assignment = find_assignment(assignment_id) + # Extracts the questionnaires + @questions = filter_questionnaires(@assignment) + @scores = Response.review_grades(@assignment, @questions) + @review_score_count = @scores[:teams].length # After rejecting nil scores need original length to iterate over hash + @averages = Response.extract_team_averages(@scores[:teams]) + @avg_of_avg = Response.average_team_scores(@averages) end - def redirect_to_review(review_mapping) - if review_mapping.new_record? - redirect_to controller: 'response', action: 'new', id: review_mapping.map_id, return: 'instructor' - else - review = Response.find_by(map_id: review_mapping.map_id) - redirect_to controller: 'response', action: 'edit', id: review.id, return: 'instructor' + # Private Method associated with edit_participant_scores: + # This method retrieves all questions from relevant questionnaires associated with this assignment. + def list_questions(assignment) + assignment.questionnaires.each_with_object({}) do |questionnaire, questions| + questions[questionnaire.id.to_s] = questionnaire.questions end end - - -# Helper methods for update - + # Private Method associated with Update methods: + # Displays an error message if the participant is not found. def handle_not_found flash[:error] = 'Participant not found.' end + # Checks if the participant's grade has changed compared to the new grade. def grade_changed?(participant, new_grade) return false if new_grade.nil? format('%.2f', params[:total_score]) != new_grade end + # Generates a message based on whether the participant's grade is present or computed. def grade_message(participant) participant.grade.nil? ? "The computed score will be used for #{participant.user.name}." : "A score of #{participant.grade}% has been saved for #{participant.user.name}." end -# These could go in a helper method - def find_participant(participant_id) - AssignmentParticipant.find(participant_id) - rescue ActiveRecord::RecordNotFound - flash[:error] = "Assignment participant #{participant_id} not found" - nil + # Private Methods associated with instructor_review: + # Finds or creates a reviewer for the given user and assignment, and sets a handle if it's a new record + def find_or_create_reviewer(user_id, assignment_id) + reviewer = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) + reviewer.set_handle if reviewer.new_record? + reviewer end - - def find_assignment(assignment_id) - Assignment.find(assignment_id) - rescue ActiveRecord::RecordNotFound - flash[:error] = "Assignment participant #{assignment_id} not found" - nil + # Finds or creates a review mapping between the reviewee and reviewer for the given assignment. + def find_or_create_review_mapping(reviewee_id, reviewer_id, assignment_id) + ReviewResponseMap.find_or_create_by(reviewee_id: reviewee_id, reviewer_id: reviewer_id, reviewed_object_id: assignment_id) end - - - # Helper methods for views - def filter_questionnaires(assignment) - questionnaires = assignment.questionnaires - if assignment.varying_rubrics_by_round? - retrieve_questions(questionnaires, assignment.id) + # Redirects to the appropriate review page based on whether the review mapping is new or existing. + def redirect_to_review(review_mapping) + if review_mapping.new_record? + redirect_to controller: 'response', action: 'new', id: review_mapping.map_id, return: 'instructor' else - questions = {} - questionnaires.each do |questionnaire| - questions[questionnaire.id.to_s.to_sym] = questionnaire.questions - end - questions + review = Response.find_by(map_id: review_mapping.map_id) + redirect_to controller: 'response', action: 'edit', id: review.id, return: 'instructor' end end - def get_data_for_heat_map(assignment_id) - # Finds the assignment - @assignment = find_assignment(assignment_id) - # Extracts the questionnaires - @questions = filter_questionnaires(@assignment) - @scores = Response.review_grades(@assignment, @questions) - @review_score_count = @scores[:teams].length # After rejecting nil scores need original length to iterate over hash - @averages = Response.extract_team_averages(@scores[:teams]) - @avg_of_avg = Response.average_team_scores(@averages) - end +end + + + diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index 67091b6b9..1eb26339c 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -1,6 +1,7 @@ module GradesHelper include PenaltyHelper + # Calculates and applies penalties for participants of a given assignment. def penalties(assignment_id) @assignment = Assignment.find(assignment_id) calculate_for_participants = should_calculate_penalties? @@ -19,51 +20,51 @@ def penalties(assignment_id) mark_penalty_as_calculated unless @assignment.is_penalty_calculated end - - private - - def should_calculate_penalties? - !@assignment.is_penalty_calculated - end - - def calculate_total_penalty(penalties) - total = penalties[:submission] + penalties[:review] + penalties[:meta_review] - total > 0 ? total : 0 - end - - def apply_max_penalty(total_penalty) - late_policy = LatePolicy.find(@assignment.late_policy_id) - total_penalty > late_policy.max_penalty ? late_policy.max_penalty : total_penalty + + # Calculates and applies penalties for the current assignment. + def fetch_penalties + penalties(@assignment.id) end - - def mark_penalty_as_calculated - @assignment.update(is_penalty_calculated: true) + + # Retrieves the name of the current user's role, if available. + def current_role_name + current_role.try :name end + # Retrieves questions from the given questionnaires for the specified assignment, considering the round if applicable. + def retrieve_questions(questionnaires, assignment_id) + questions = {} + questionnaires.each do |questionnaire| + round = AssignmentQuestionnaire.where(assignment_id: assignment_id, questionnaire_id: questionnaire.id).first.used_in_round + questionnaire_symbol = if round.nil? + questionnaire.symbol + else + (questionnaire.symbol.to_s + round.to_s).to_sym + end + questions[questionnaire_symbol] = questionnaire.questions + end + questions + end - # Helper to retrieve participant and related assignment data + # Retrieves the participant and their associated assignment data. def fetch_participant_and_assignment @participant = AssignmentParticipant.find(params[:id]) @assignment = @participant.assignment end - # Helper to retrieve questionnaires and questions + # Retrieves the questionnaires and their associated questions for the assignment. def fetch_questionnaires_and_questions questionnaires = @assignment.questionnaires @questions = retrieve_questions(questionnaires, @assignment.id) end - # Helper to fetch participant scores + # Fetches the scores for the participant based on the retrieved questions. def fetch_participant_scores @pscore = participant_scores(@participant, @questions) end - # Helper to calculate penalties - def fetch_penalties - penalties(@assignment.id) - end - # Helper to summarize reviews by reviewee + # Summarizes the feedback received by the reviewee, including overall summary and average scores by round and criterion. def fetch_feedback_summary summary_ws_url = WEBSERVICE_CONFIG['summary_webservice_url'] sum = SummaryHelper::Summary.new.summarize_reviews_by_reviewee(@questions, @assignment, @team_id, summary_ws_url, session) @@ -72,6 +73,7 @@ def fetch_feedback_summary @avg_scores_by_criterion = sum.avg_scores_by_criterion end + # Processes questionnaires for a team, considering topic-specific and round-specific rubrics, and populates view models accordingly. def process_questionare_for_team(assignment, team_id) vmlist = [] @@ -105,7 +107,7 @@ def process_questionare_for_team(assignment, team_id) return vmlist end - + # Redirects the user if they are not allowed to access the assignment, based on team or reviewer authorization. def redirect_when_disallowed if team_assignment? redirect_if_not_on_correct_team @@ -113,14 +115,66 @@ def redirect_when_disallowed redirect_if_not_authorized_reviewer end false + end + + # Populates the view model with questionnaire data, team members, reviews, and calculated metrics. + def populate_view_model(questionnaire) + vm = VmQuestionResponse.new(questionnaire, @assignment, @round) + vmquestions = questionnaire.questions + vm.add_questions(vmquestions) + vm.add_team_members(@team) + qn = AssignmentQuestionnaire.where(assignment_id: @assignment.id, used_in_round: 2).size >= 1 + vm.add_reviews(@participant, @team, @assignment.varying_rubrics_by_round?) + vm.calculate_metrics + vm + end + + # Finds an assignment participant by ID, and handles the case where the participant is not found. + def find_participant(participant_id) + AssignmentParticipant.find(participant_id) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Assignment participant #{participant_id} not found" + nil end + # Finds an assignment participant by ID, and handles the case where the participant is not found. + def find_assignment(assignment_id) + Assignment.find(assignment_id) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Assignment participant #{assignment_id} not found" + nil + end + private + # Determines if penalties should be calculated based on the assignment's penalty status. + def should_calculate_penalties? + !@assignment.is_penalty_calculated + end + + # Calculates the total penalty from submission, review, and meta-review penalties. + def calculate_total_penalty(penalties) + total = penalties[:submission] + penalties[:review] + penalties[:meta_review] + total > 0 ? total : 0 + end + + # Applies the maximum penalty limit based on the assignment's late policy. + def apply_max_penalty(total_penalty) + late_policy = LatePolicy.find(@assignment.late_policy_id) + total_penalty > late_policy.max_penalty ? late_policy.max_penalty : total_penalty + end + + # Marks the assignment's penalty status as calculated. + def mark_penalty_as_calculated + @assignment.update(is_penalty_calculated: true) + end + + # Checks if the assignment is a team assignment based on the maximum team size. def team_assignment? @participant.assignment.max_team_size > 1 end + # Redirects the user if they are not on the correct team that provided the feedback. def redirect_if_not_on_correct_team team = @participant.team if team.nil? || !team.user?(session[:user]) @@ -129,6 +183,7 @@ def redirect_if_not_on_correct_team end end + # Redirects the user if they are not an authorized reviewer for the feedback. def redirect_if_not_authorized_reviewer reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first return if current_user_id?(reviewer.try(:user_id)) @@ -137,15 +192,5 @@ def redirect_if_not_authorized_reviewer redirect_to '/' end - def populate_view_model(questionnaire) - vm = VmQuestionResponse.new(questionnaire, @assignment, @round) - vmquestions = questionnaire.questions - vm.add_questions(vmquestions) - vm.add_team_members(@team) - qn = AssignmentQuestionnaire.where(assignment_id: @assignment.id, used_in_round: 2).size >= 1 - vm.add_reviews(@participant, @team, @assignment.varying_rubrics_by_round?) - vm.calculate_metrics - vm - end end \ No newline at end of file From 3078ba0c4228d4c49ef8b7ef75b67190c18dc90b Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 14:46:52 -0400 Subject: [PATCH 21/41] Rspec test action_allowed for TA --- app/controllers/api/v1/grades_controller.rb | 4 + config/routes.rb | 6 ++ .../requests/api/v1/grades_controller_spec.rb | 94 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 spec/requests/api/v1/grades_controller_spec.rb diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index d380e7dc1..e76733a30 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -24,10 +24,14 @@ class Api::V1::GradesController < ApplicationController 'view_team' => :student_or_ta? }.freeze +<<<<<<< Updated upstream # Determines if the current user is allowed to perform the specified action. # Checks the permission using the action parameter and returns a JSON response. # If the action is permitted, the response status is :ok; otherwise, it is :forbidden. def action_allowed? +======= + def action_allowed +>>>>>>> Stashed changes permitted = check_permission(params[:action]) render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end diff --git a/config/routes.rb b/config/routes.rb index 33df803e7..1e43f33c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -120,6 +120,12 @@ delete '/:id', to: 'participants#destroy' end end + + resources :grades do + collection do + get 'action_allowed' + end + end end end end \ No newline at end of file diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb new file mode 100644 index 000000000..47be7ecc9 --- /dev/null +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -0,0 +1,94 @@ +require 'swagger_helper' +require 'rails_helper' +require 'json_web_token' + +RSpec.describe Api::V1::GradesController, type: :controller do + before(:all) do + @roles = create_roles_hierarchy + end + + let!(:ta) do + User.create!( + name: "ta", + password_digest: "password", + role_id: @roles[:ta].id, + full_name: "name", + email: "ta@example.com" + ) + end + + let!(:s1) do + User.create!( + name: "s1", + password_digest: "password", + role_id: @roles[:student].id, + full_name: "s1", + email: "s1@example.com" + ) + end + + let(:ta_token) { JsonWebToken.encode({id: ta.id}) } + let(:student_token) { JsonWebToken.encode({id: s1.id}) } + + describe '#action_allowed' do + context 'when the user is a Teaching Assistant' do + it 'allows access to view_team to a TA' do + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + get :action_allowed, params: { action: 'view_team' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) + end + end + + context 'when the user is a Student' do + it 'allows access to view_team if student is viewing their own team' do + allow_any_instance_of(Api::V1::GradesController).to receive(:student_viewing_own_team?).and_return(true) + + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + get :action_allowed, params: { action: 'view_team' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) + end + + it 'denies access to view_team if student is not viewing their own team' do + allow_any_instance_of(Api::V1::GradesController).to receive(:student_viewing_own_team?).and_return(false) + + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + get :action_allowed, params: { action: 'view_team' } + + expect(response).to have_http_status(:forbidden) + expect(JSON.parse(response.body)).to eq({ 'allowed' => false }) + end + + # it 'allows access to view_my_scores if student has finished self review and has proper authorizations' do + # # Ensure that s1 meets the criteria for 'view_my_scores' + # allow_any_instance_of(Api::V1::GradesController).to receive(:self_review_finished?).and_return(true) + # allow_any_instance_of(Api::V1::GradesController).to receive(:are_needed_authorizations_present?).and_return(true) + + # request.headers['Authorization'] = "Bearer #{student_token}" + # request.headers['Content-Type'] = 'application/json' + # get :action_allowed, params: { action: 'view_my_scores' } + + # expect(response).to have_http_status(:ok) + # expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) + # end + + # it 'denies access to view_my_scores if student has not finished self review or lacks authorizations' do + # # Simulate conditions where the student can't view scores + # allow_any_instance_of(Api::V1::GradesController).to receive(:self_review_finished?).and_return(false) + + # request.headers['Authorization'] = "Bearer #{student_token}" + # request.headers['Content-Type'] = 'application/json' + # get :action_allowed, params: { action: 'view_my_scores' } + + # expect(response).to have_http_status(:forbidden) + # expect(JSON.parse(response.body)).to eq({ 'allowed' => false }) + # end + end + end +end From e6ca7bdb35b0ce754a4b492a803c1ef70e7fdb4a Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 14:48:23 -0400 Subject: [PATCH 22/41] resolved merge conflict --- app/controllers/api/v1/grades_controller.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index e76733a30..45ac0f8d8 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -24,14 +24,7 @@ class Api::V1::GradesController < ApplicationController 'view_team' => :student_or_ta? }.freeze -<<<<<<< Updated upstream - # Determines if the current user is allowed to perform the specified action. - # Checks the permission using the action parameter and returns a JSON response. - # If the action is permitted, the response status is :ok; otherwise, it is :forbidden. - def action_allowed? -======= def action_allowed ->>>>>>> Stashed changes permitted = check_permission(params[:action]) render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end From 17e44873a6656345bdc4d8b3d88438a992428e83 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 14:53:55 -0400 Subject: [PATCH 23/41] Removed the extra end and the end of the controller --- app/controllers/api/v1/grades_controller.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 45ac0f8d8..1799f6abb 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -270,11 +270,3 @@ def redirect_to_review(review_mapping) end - - - - - -end - - From 1ee167f77289697b3ff66cb6e6377342a908d678 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 15:51:55 -0400 Subject: [PATCH 24/41] Completed test for action_allowed --- app/controllers/api/v1/grades_controller.rb | 2 +- .../requests/api/v1/grades_controller_spec.rb | 63 +++++++++---------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 1799f6abb..7d8f8915d 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -25,7 +25,7 @@ class Api::V1::GradesController < ApplicationController }.freeze def action_allowed - permitted = check_permission(params[:action]) + permitted = check_permission(params[:requested_action]) render json: { allowed: permitted }, status: permitted ? :ok : :forbidden end diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index 47be7ecc9..214b9f91e 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -17,15 +17,15 @@ ) end - let!(:s1) do - User.create!( - name: "s1", - password_digest: "password", - role_id: @roles[:student].id, - full_name: "s1", - email: "s1@example.com" - ) - end + let(:s1) { + User.create( + name: "studenta", + password_digest: "password", + role_id: @roles[:student].id, + full_name: "student A", + email: "testuser@example.com" + ) + } let(:ta_token) { JsonWebToken.encode({id: ta.id}) } let(:student_token) { JsonWebToken.encode({id: s1.id}) } @@ -35,7 +35,7 @@ it 'allows access to view_team to a TA' do request.headers['Authorization'] = "Bearer #{ta_token}" request.headers['Content-Type'] = 'application/json' - get :action_allowed, params: { action: 'view_team' } + get :action_allowed, params: { requested_action: 'view_team' } expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) @@ -43,12 +43,13 @@ end context 'when the user is a Student' do - it 'allows access to view_team if student is viewing their own team' do + it 'allows access to view_team if student is viewing their own team' do allow_any_instance_of(Api::V1::GradesController).to receive(:student_viewing_own_team?).and_return(true) + allow_any_instance_of(Api::V1::GradesController).to receive(:student_or_ta?).and_return(true) request.headers['Authorization'] = "Bearer #{student_token}" request.headers['Content-Type'] = 'application/json' - get :action_allowed, params: { action: 'view_team' } + get :action_allowed, params: { requested_action: 'view_team' } expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) @@ -59,36 +60,34 @@ request.headers['Authorization'] = "Bearer #{student_token}" request.headers['Content-Type'] = 'application/json' - get :action_allowed, params: { action: 'view_team' } + get :action_allowed, params: { requested_action: 'view_team' } expect(response).to have_http_status(:forbidden) expect(JSON.parse(response.body)).to eq({ 'allowed' => false }) end - # it 'allows access to view_my_scores if student has finished self review and has proper authorizations' do - # # Ensure that s1 meets the criteria for 'view_my_scores' - # allow_any_instance_of(Api::V1::GradesController).to receive(:self_review_finished?).and_return(true) - # allow_any_instance_of(Api::V1::GradesController).to receive(:are_needed_authorizations_present?).and_return(true) + it 'allows access to view_my_scores if student has finished self review and has proper authorizations' do + allow_any_instance_of(Api::V1::GradesController).to receive(:self_review_finished?).and_return(true) + allow_any_instance_of(Api::V1::GradesController).to receive(:are_needed_authorizations_present?).and_return(true) - # request.headers['Authorization'] = "Bearer #{student_token}" - # request.headers['Content-Type'] = 'application/json' - # get :action_allowed, params: { action: 'view_my_scores' } + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + get :action_allowed, params: { requested_action: 'view_my_scores' } - # expect(response).to have_http_status(:ok) - # expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) - # end + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({ 'allowed' => true }) + end - # it 'denies access to view_my_scores if student has not finished self review or lacks authorizations' do - # # Simulate conditions where the student can't view scores - # allow_any_instance_of(Api::V1::GradesController).to receive(:self_review_finished?).and_return(false) + it 'denies access to view_my_scores if student has not finished self review or lacks authorizations' do + allow_any_instance_of(Api::V1::GradesController).to receive(:self_review_finished?).and_return(false) - # request.headers['Authorization'] = "Bearer #{student_token}" - # request.headers['Content-Type'] = 'application/json' - # get :action_allowed, params: { action: 'view_my_scores' } + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + get :action_allowed, params: { requested_action: 'view_my_scores' } - # expect(response).to have_http_status(:forbidden) - # expect(JSON.parse(response.body)).to eq({ 'allowed' => false }) - # end + expect(response).to have_http_status(:forbidden) + expect(JSON.parse(response.body)).to eq({ 'allowed' => false }) + end end end end From bdc3901a7e6a3cfc20b8479969b3d7227627e37f Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 16:48:57 -0400 Subject: [PATCH 25/41] Added missing function in penalties --- app/controllers/api/v1/grades_controller.rb | 4 ++-- app/helpers/grades_helper.rb | 11 ++++++++++- spec/requests/api/v1/grades_controller_spec.rb | 18 +++++++++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 7d8f8915d..3e68bc3ef 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -35,7 +35,7 @@ def action_allowed # difference in scores among the reviews. def view_grading_report get_data_for_heat_map(params[:id]) - fetch_penalties + update_penalties @show_reputation = false end @@ -53,7 +53,7 @@ def view_my_scores @topic_id = SignedUpTeam.topic_id(@participant.assignment.id, @participant.user_id) @stage = @participant.assignment.current_stage(@topic_id) - fetch_penalties + update_penalties # prepare feedback summaries fetch_feedback_summary diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index 1eb26339c..fdef3696f 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -22,7 +22,7 @@ def penalties(assignment_id) end # Calculates and applies penalties for the current assignment. - def fetch_penalties + def update_penalties penalties(@assignment.id) end @@ -169,6 +169,15 @@ def mark_penalty_as_calculated @assignment.update(is_penalty_calculated: true) end + def assign_all_penalties(participant, penalties) + @all_penalties[participant.id] = { + submission: penalties[:submission], + review: penalties[:review], + meta_review: penalties[:meta_review], + total_penalty: @total_penalty + } + end + # Checks if the assignment is a team assignment based on the maximum team size. def team_assignment? @participant.assignment.max_team_size > 1 diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index 214b9f91e..332cb2d42 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -17,15 +17,15 @@ ) end - let(:s1) { - User.create( - name: "studenta", - password_digest: "password", - role_id: @roles[:student].id, - full_name: "student A", - email: "testuser@example.com" - ) - } + let(:s1) do + User.create( + name: "studenta", + password_digest: "password", + role_id: @roles[:student].id, + full_name: "student A", + email: "testuser@example.com" + ) + end let(:ta_token) { JsonWebToken.encode({id: ta.id}) } let(:student_token) { JsonWebToken.encode({id: s1.id}) } From 8c512eb1deb2407296219cfe9b5a6ca6c541fb30 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 19:16:45 -0400 Subject: [PATCH 26/41] Added test for instructor_review --- app/controllers/api/v1/grades_controller.rb | 14 --- app/models/response.rb | 7 +- config/routes.rb | 2 + .../requests/api/v1/grades_controller_spec.rb | 113 +++++++++++++++++- 4 files changed, 118 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 3e68bc3ef..e76881da0 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,20 +1,6 @@ class Api::V1::GradesController < ApplicationController include GradesHelper - # def action_allowed? - # permitted = case params[:action] - # when 'view_my_scores' - # student_with_permissions? - # when 'view_team' - # student_viewing_own_team? || has_privileges_of?('Teaching Assistant') - # else - # has_privileges_of?('Teaching Assistant') - # end - - # render json: { allowed: permitted }, status: permitted ? :ok : :forbidden - # end - - # Defines permissions for different actions based on user roles. # 'view_my_scores' is allowed for students with specific permissions. # 'view_team' is allowed for both students and TAs. diff --git a/app/models/response.rb b/app/models/response.rb index 5fd159c5d..eed6978ab 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -322,9 +322,8 @@ def compute_avg_and_ranges_hash(assignment) end scores end -end -private + private # Get all of the questions asked during peer review for the given team's work def peer_review_questions_for_team(assignment, team, round_number = nil) @@ -419,5 +418,7 @@ def merge_grades_by_rounds(assignment, grades_by_rounds, num_of_assessments, tot end team_scores end - end + + + diff --git a/config/routes.rb b/config/routes.rb index 1e43f33c9..fcd3640d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -124,6 +124,8 @@ resources :grades do collection do get 'action_allowed' + get 'view_grading_report' + get 'instructor_review' end end end diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index 332cb2d42..a53bb98a3 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -17,7 +17,7 @@ ) end - let(:s1) do + let!(:s1) do User.create( name: "studenta", password_digest: "password", @@ -26,9 +26,38 @@ email: "testuser@example.com" ) end + let!(:s2) do + User.create( + name: "studentb", + password_digest: "password", + role_id: @roles[:student].id, + full_name: "student B", + email: "testusebr@example.com" + ) + end + + let!(:prof) do + User.create!( + name: "profa", + password_digest: "password", + role_id: @roles[:instructor].id, + full_name: "Prof A", + email: "testuser@example.com", + mru_directory_path: "/home/testuser" + ) + end let(:ta_token) { JsonWebToken.encode({id: ta.id}) } let(:student_token) { JsonWebToken.encode({id: s1.id}) } + + + let!(:assignment) { Assignment.create!(name: 'Test Assignment',instructor_id: prof.id) } + let!(:team) { Team.create!(id: 1, assignment_id: assignment.id) } + let!(:participant) { AssignmentParticipant.create!(user: s1, assignment_id: assignment.id, team: team, handle: 'handle') } + let!(:questionnaire) { Questionnaire.create!(name: 'Review Questionnaire',max_question_score:100,min_question_score:0,instructor_id:prof.id) } + let!(:assignment_questionnaire) { AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire) } + # let!(:question) { Question.create!(questionnaire: questionnaire, txt: 'Question 1', type: 'Criterion', seq: 1) } + # let!(:question) { questionnaire.items.create!(txt: 'Question 1', seq: 1) } describe '#action_allowed' do context 'when the user is a Teaching Assistant' do @@ -90,4 +119,86 @@ end end end + + describe '#instructor_review' do + let!(:instructor) do + User.create!( + name: "profn", + password_digest: "password", + role_id: @roles[:instructor].id, + full_name: "Prof n", + email: "testussder@example.com", + mru_directory_path: "/home/testuser" + ) + end + + let(:instructor_token) { JsonWebToken.encode({ id: instructor.id }) } + let!(:participant) { AssignmentParticipant.create!(user: s1, assignment_id: assignment.id, team: team, handle: 'handle') } + let!(:participant2) { AssignmentParticipant.create!(user: s2, assignment_id: assignment.id, team: team, handle: 'handle') } + + let(:assignment_team) { Team.create!(assignment_id: assignment.id) } + let(:reviewer) { participant } + let(:reviewee) { participant2 } + + let!(:review_response_map) do + ReviewResponseMap.create!( + assignment: assignment, + reviewer: reviewer, + reviewee: assignment_team + ) + end + + let!(:response) do + Response.create!( + response_map: review_response_map, + additional_comment: nil, + is_submitted: false + ) + end + + context 'when review exists' do + it 'redirects to response#edit page' do + # Stubbing methods for find_participant, find_or_create_reviewer, and find_or_create_review_mapping + allow_any_instance_of(Api::V1::GradesController).to receive(:find_participant).with('1').and_return(participant) + allow_any_instance_of(Api::V1::GradesController).to receive(:find_or_create_reviewer).with(instructor.id, participant.assignment.id).and_return(participant) + allow_any_instance_of(Api::V1::GradesController).to receive(:find_or_create_review_mapping).with(participant.team.id, participant.id, participant.assignment.id).and_return(review_response_map) + allow(review_response_map).to receive(:new_record?).and_return(false) + allow(Response).to receive(:find_by).with(map_id: review_response_map.map_id).and_return(response) + allow(controller).to receive(:redirect_to_review) + + request_params = { id: 1 } + user_session = { user: instructor } + + request.headers['Authorization'] = "Bearer #{instructor_token}" + request.headers['Content-Type'] = 'application/json' + + get :instructor_review, params: request_params, session: user_session + + expect(controller).to have_received(:redirect_to_review).with(review_response_map) + end + end + + context 'when review does not exist' do + it 'redirects to response#new page' do + # Stubbing methods for find_participant, find_or_create_reviewer, and find_or_create_review_mapping + allow_any_instance_of(Api::V1::GradesController).to receive(:find_participant).with('1').and_return(participant2) + allow_any_instance_of(Api::V1::GradesController).to receive(:find_or_create_reviewer).with(instructor.id, participant2.assignment.id).and_return(participant2) + allow_any_instance_of(Api::V1::GradesController).to receive(:find_or_create_review_mapping).with(participant2.team.id, participant2.id, participant2.assignment.id).and_return(review_response_map) + allow(review_response_map).to receive(:new_record?).and_return(true) + allow(Response).to receive(:find_by).with(map_id: review_response_map.map_id).and_return(response) + allow(controller).to receive(:redirect_to_review) + + request_params = { id: 1 } + user_session = { user: instructor } + + request.headers['Authorization'] = "Bearer #{instructor_token}" + request.headers['Content-Type'] = 'application/json' + + get :instructor_review, params: request_params, session: user_session + + expect(controller).to have_received(:redirect_to_review).with(review_response_map) + end + end + end + end From e0c8506e2728639d37795f659c00553ec33420a1 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 20:50:08 -0400 Subject: [PATCH 27/41] changed update_team method to avoid DoubleRenderError and added rspec for the method --- app/controllers/api/v1/grades_controller.rb | 12 +++-- config/routes.rb | 1 + .../requests/api/v1/grades_controller_spec.rb | 50 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index e76881da0..99841af5e 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -105,15 +105,17 @@ def update_participant_grade def update_team participant = AssignmentParticipant.find_by(id: params[:participant_id]) return handle_not_found unless participant - + if participant.team.update(grade_for_submission: params[:grade_for_submission], comment_for_submission: params[:comment_for_submission]) - flash[:success] = 'Grade and comment for submission successfully saved.' + render json: { message: 'Grade and comment for submission successfully saved.' }, status: :ok + return else - flash[:error] = 'Error saving grade and comment.' + render json: { error: 'Error saving grade and comment.' }, status: :unprocessable_entity + return end - redirect_to controller: 'grades', action: 'view_team', id: params[:id] end + # Determines the appropriate controller action for an instructor's review based on the current state. # This method checks if a review mapping exists for the participant. If it does, the instructor is directed to edit the existing review. @@ -213,7 +215,7 @@ def list_questions(assignment) # Private Method associated with Update methods: # Displays an error message if the participant is not found. def handle_not_found - flash[:error] = 'Participant not found.' + render json: { error: 'Participant not found.' }, status: :not_found end # Checks if the participant's grade has changed compared to the new grade. diff --git a/config/routes.rb b/config/routes.rb index fcd3640d6..c08b3f1d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,6 +126,7 @@ get 'action_allowed' get 'view_grading_report' get 'instructor_review' + post 'update_team' end end end diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index a53bb98a3..aba4aa1bf 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -201,4 +201,54 @@ end end + describe '#update_team' do + context 'when participant is found and update is successful' do + it 'updates grade and comment for submission and redirects to grades#view_team page' do + allow(AssignmentParticipant).to receive(:find_by).with(id: 1).and_return(participant) + allow(participant.team).to receive(:update).with(grade_for_submission: 100, comment_for_submission: 'comment').and_return(true) + + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + + request_params = {participant_id: 1,grade_for_submission: 100,comment_for_submission: 'comment' } + + post :update_team, params: request_params + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({ "message" => "Grade and comment for submission successfully saved." }) end + end + + context 'when participant is not found' do + it 'returns a JSON error message with status 404' do + allow(AssignmentParticipant).to receive(:find_by).with(id: 1).and_return(nil) + + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + request_params = { participant_id: 1, grade_for_submission: 100, comment_for_submission: 'comment' } + + post :update_team, params: request_params + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)).to eq({ "error" => "Participant not found." }) + end + end + + context 'when update fails' do + it 'returns a JSON error message with status 422' do + allow(AssignmentParticipant).to receive(:find_by).with(id: 1).and_return(participant) + allow(participant.team).to receive(:update).with(grade_for_submission: 100, comment_for_submission: 'comment').and_return(false) + + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + request_params = { participant_id: 1, grade_for_submission: 100, comment_for_submission: 'comment' } + + post :update_team, params: request_params + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to eq({ "error" => "Error saving grade and comment." }) + end + end + end + + end From 86b51350a19b102c207661214d82ea09f5a5f5b6 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sat, 22 Mar 2025 22:00:28 -0400 Subject: [PATCH 28/41] Added grade column to Participant column, updated update_participant_score function to avoid double render error, rspec for update_participant_score --- app/controllers/api/v1/grades_controller.rb | 7 +- app/models/participant.rb | 1 + config/routes.rb | 1 + ...0250323015142_add_grade_to_participants.rb | 5 ++ db/schema.rb | 3 +- .../requests/api/v1/grades_controller_spec.rb | 74 +++++++++++++++++++ 6 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20250323015142_add_grade_to_participants.rb diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 99841af5e..33c054a66 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -88,14 +88,13 @@ def update_participant_grade return handle_not_found unless participant new_grade = params[:participant][:grade] + if grade_changed?(participant, new_grade) participant.update(grade: new_grade) - flash[:note] = grade_message(participant) + render json: { message: grade_message(participant) }, status: :ok else - flash[:error] = 'Error updating participant grade.' + render json: { error: 'Error updating participant grade.' }, status: :unprocessable_entity end - # Redirect to the edit action for the participant. - redirect_to action: 'edit', id: params[:id] end # Update the grade and comment for a participant's submission. diff --git a/app/models/participant.rb b/app/models/participant.rb index cce037f0b..212981686 100644 --- a/app/models/participant.rb +++ b/app/models/participant.rb @@ -10,6 +10,7 @@ class Participant < ApplicationRecord # Validations validates :user_id, presence: true validates :assignment_id, presence: true + validates :grade, numericality: { allow_nil: true } # Methods def fullname diff --git a/config/routes.rb b/config/routes.rb index c08b3f1d6..fbffa59cf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,6 +127,7 @@ get 'view_grading_report' get 'instructor_review' post 'update_team' + post 'update_participant_grade' end end end diff --git a/db/migrate/20250323015142_add_grade_to_participants.rb b/db/migrate/20250323015142_add_grade_to_participants.rb new file mode 100644 index 000000000..f07534102 --- /dev/null +++ b/db/migrate/20250323015142_add_grade_to_participants.rb @@ -0,0 +1,5 @@ +class AddGradeToParticipants < ActiveRecord::Migration[8.0] + def change + add_column :participants, :grade, :float + end +end diff --git a/db/schema.rb b/db/schema.rb index 7db16863e..b1ff40c8b 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_02_16_020117) do +ActiveRecord::Schema[8.0].define(version: 2025_03_23_015142) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -231,6 +231,7 @@ t.boolean "can_take_quiz" t.boolean "can_mentor" t.string "authorization" + t.float "grade" t.index ["assignment_id"], name: "index_participants_on_assignment_id" t.index ["join_team_request_id"], name: "index_participants_on_join_team_request_id" t.index ["team_id"], name: "index_participants_on_team_id" diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index aba4aa1bf..0b8bc91f6 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -250,5 +250,79 @@ end end + describe '#update_participant_grade' do + context 'when participant is not found' do + it 'returns a 404 response with an error message' do + allow(AssignmentParticipant).to receive(:find_by).with(id: 1).and_return(nil) + + request_params = { + id: 1, + participant: { grade: 100 } + } + + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + + # Perform the request + post :update_participant_grade, params: request_params + + # Expecting the response to return 404 with error message + expect(response.status).to eq(404) + expect(JSON.parse(response.body)['error']).to eq('Participant not found.') + end + end + + context 'when grade is updated successfully' do + it 'returns a success message with a 200 status' do + allow(participant).to receive(:update).with(grade: 100).and_return(true) + + request_params = { + id: participant.id, + participant: { grade: 100 }, + total_score: 100.00 + } + + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + + post :update_participant_grade, params: request_params + + new_grade = 100.00 + # Use request_params[:total_score] instead of params[:total_score] + expect(format('%.2f', request_params[:total_score].to_f)).to eq(format('%.2f', new_grade)) + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)['message']).to eq("A score of 100.0% has been saved for studenta.") + end + end + + context 'when grade update fails' do + it 'returns an error message with a 422 status' do + allow(participant).to receive(:update).and_return(false) + allow(participant).to receive(:errors).and_return(double(full_messages: ['Grade cannot be blank'])) + + request_params = { + id: participant.id, + participant: { grade: nil }, + total_score: nil + } + + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + + post :update_participant_grade, params: request_params + + new_grade = 100.00 + formatted_total_score = request_params[:total_score].nil? ? '0.00' : format('%.2f', request_params[:total_score].to_f) + + expect(formatted_total_score).not_to eq(format('%.2f', new_grade)) + + expect(response.status).to eq(422) + expect(JSON.parse(response.body)['message']).to eq(nil) + end + end + + end + end From aac04fd2c0cfc1d637e75400a5ebfb08c950ffa4 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sun, 23 Mar 2025 01:00:37 -0400 Subject: [PATCH 29/41] Added Question Model, rspec foredit_participant_scores --- Gemfile | 1 + Gemfile.lock | 5 ++ app/models/question.rb | 16 ++++++ app/models/questionnaire.rb | 1 + app/models/response.rb | 2 +- config/routes.rb | 1 + ....rb => 20250323035425_create_questions.rb} | 4 +- ...23040421_create_review_of_review_scores.rb | 28 ++++++++++ db/schema.rb | 20 +++++++- .../requests/api/v1/grades_controller_spec.rb | 51 ++++++++++++++----- 10 files changed, 112 insertions(+), 17 deletions(-) create mode 100644 app/models/question.rb rename db/migrate/{20230401213404_create_questions.rb => 20250323035425_create_questions.rb} (89%) create mode 100644 db/migrate/20250323040421_create_review_of_review_scores.rb diff --git a/Gemfile b/Gemfile index b5f47ee4b..2938b11ee 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,7 @@ group :development, :test do gem 'simplecov', require: false, group: :test gem 'coveralls' gem 'simplecov_json_formatter' + gem 'rails-controller-testing' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 9cca8ef5d..2e44cf40f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -207,6 +207,10 @@ GEM activesupport (= 8.0.1) bundler (>= 1.15.0) railties (= 8.0.1) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -330,6 +334,7 @@ DEPENDENCIES puma (~> 5.0) rack-cors rails (~> 8.0, >= 8.0.1) + rails-controller-testing rspec-rails rswag-api rswag-specs diff --git a/app/models/question.rb b/app/models/question.rb new file mode 100644 index 000000000..f217161b2 --- /dev/null +++ b/app/models/question.rb @@ -0,0 +1,16 @@ +class Question < ApplicationRecord + belongs_to :questionnaire # each question belongs to a specific questionnaire + # belongs_to :review_of_review_score # ditto + has_many :question_advices, dependent: :destroy # for each question, there is separate advice about each possible score + has_many :signup_choices # this may reference signup type questionnaires + has_many :answers, dependent: :destroy + + validates :seq, presence: true # user must define sequence for a question + validates :seq, numericality: true # sequence must be numeric + validates :txt, length: { minimum: 0, allow_nil: false, message: "can't be nil" } # user must define text content for a question + # validates :type, presence: true # user must define type for a question + validates :break_before, presence: true + + +end + \ No newline at end of file diff --git a/app/models/questionnaire.rb b/app/models/questionnaire.rb index 1e8ff0aa7..43963ad66 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -2,6 +2,7 @@ class Questionnaire < ApplicationRecord belongs_to :instructor has_many :items, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire before_destroy :check_for_question_associations + has_many :questions validate :validate_questionnaire validates :name, presence: true diff --git a/app/models/response.rb b/app/models/response.rb index eed6978ab..aa694fc37 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -157,7 +157,7 @@ def compute_total_score(assignment, scores) # } # } # Called in: grades_controller.rb (view), assignment.rb (self.export) - def review_grades(assignment, questions) + def self.review_grades(assignment, questions) scores = { participants: {}, teams: {} } assignment.participants.each do |participant| scores[:participants][participant.id.to_s.to_sym] = participant_scores(participant, questions) diff --git a/config/routes.rb b/config/routes.rb index fbffa59cf..39073a411 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -128,6 +128,7 @@ get 'instructor_review' post 'update_team' post 'update_participant_grade' + get 'edit_participant_scores' end end end diff --git a/db/migrate/20230401213404_create_questions.rb b/db/migrate/20250323035425_create_questions.rb similarity index 89% rename from db/migrate/20230401213404_create_questions.rb rename to db/migrate/20250323035425_create_questions.rb index 2687ac23a..2930772ee 100644 --- a/db/migrate/20230401213404_create_questions.rb +++ b/db/migrate/20250323035425_create_questions.rb @@ -1,4 +1,4 @@ -class CreateQuestions < ActiveRecord::Migration[7.0] +class CreateQuestions < ActiveRecord::Migration[8.0] def change create_table :questions do |t| t.text :txt @@ -15,5 +15,7 @@ def change end add_reference :questions, :questionnaire, null: false, foreign_key: true add_index :questions, :questionnaire_id, name: :fk_question_questionnaires + end end + diff --git a/db/migrate/20250323040421_create_review_of_review_scores.rb b/db/migrate/20250323040421_create_review_of_review_scores.rb new file mode 100644 index 000000000..b4a25450d --- /dev/null +++ b/db/migrate/20250323040421_create_review_of_review_scores.rb @@ -0,0 +1,28 @@ +class CreateReviewOfReviewScores < ActiveRecord::Migration[8.0] + def change + def self.up + create_table 'review_of_review_scores', force: true do |t| + t.column 'review_of_review_id', :integer + t.column 'question_id', :integer + t.column 'score', :integer + t.column 'comments', :text + end + + add_index 'review_of_review_scores', ['review_of_review_id'], name: 'fk_review_of_review_score_reviews' + + execute "alter table review_of_review_scores + add constraint fk_review_of_review_score_reviews + foreign key (review_of_review_id) references review_of_reviews(id)" + + add_index 'review_of_review_scores', ['question_id'], name: 'fk_review_of_review_score_questions' + + execute "alter table review_of_review_scores + add constraint fk_review_of_review_score_questions + foreign key (question_id) references questions(id)" + end + + def self.down + drop_table 'review_of_review_scores' + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b1ff40c8b..c1c1ec801 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_03_23_015142) do +ActiveRecord::Schema[8.0].define(version: 2025_03_23_040421) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -273,6 +273,23 @@ t.datetime "updated_at", null: false end + create_table "questions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.text "txt" + t.integer "weight" + t.decimal "seq", precision: 10 + t.string "question_type" + t.string "size" + t.string "alternatives" + t.boolean "break_before" + t.string "max_label" + t.string "min_label" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "questionnaire_id", null: false + t.index ["questionnaire_id"], name: "fk_question_questionnaires" + t.index ["questionnaire_id"], name: "index_questions_on_questionnaire_id" + end + create_table "quiz_question_choices", id: :integer, charset: "latin1", force: :cascade do |t| t.integer "question_id" t.text "txt" @@ -400,6 +417,7 @@ add_foreign_key "participants", "teams" add_foreign_key "participants", "users" add_foreign_key "question_advices", "items", column: "question_id" + add_foreign_key "questions", "questionnaires" add_foreign_key "roles", "roles", column: "parent_id", on_delete: :cascade add_foreign_key "sign_up_topics", "assignments" add_foreign_key "signed_up_teams", "sign_up_topics" diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index 0b8bc91f6..66022f2cc 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -47,8 +47,20 @@ ) end + let!(:instructor) do + User.create!( + name: "profn", + password_digest: "password", + role_id: @roles[:instructor].id, + full_name: "Prof n", + email: "testussder@example.com", + mru_directory_path: "/home/testuser" + ) + end + let(:ta_token) { JsonWebToken.encode({id: ta.id}) } let(:student_token) { JsonWebToken.encode({id: s1.id}) } + let(:instructor_token) { JsonWebToken.encode({ id: instructor.id }) } let!(:assignment) { Assignment.create!(name: 'Test Assignment',instructor_id: prof.id) } @@ -56,8 +68,12 @@ let!(:participant) { AssignmentParticipant.create!(user: s1, assignment_id: assignment.id, team: team, handle: 'handle') } let!(:questionnaire) { Questionnaire.create!(name: 'Review Questionnaire',max_question_score:100,min_question_score:0,instructor_id:prof.id) } let!(:assignment_questionnaire) { AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire) } - # let!(:question) { Question.create!(questionnaire: questionnaire, txt: 'Question 1', type: 'Criterion', seq: 1) } - # let!(:question) { questionnaire.items.create!(txt: 'Question 1', seq: 1) } + let!(:question) { Question.create!(questionnaire: questionnaire, txt: 'Question 1', seq: 1, break_before: 1) } + # let(:review_questionnaire) { build(:questionnaire, id: 1, questions: [question]) } + let!(:review_questionnaire) do + questionnaire.update(questions: [question]) + questionnaire + end describe '#action_allowed' do context 'when the user is a Teaching Assistant' do @@ -121,18 +137,6 @@ end describe '#instructor_review' do - let!(:instructor) do - User.create!( - name: "profn", - password_digest: "password", - role_id: @roles[:instructor].id, - full_name: "Prof n", - email: "testussder@example.com", - mru_directory_path: "/home/testuser" - ) - end - - let(:instructor_token) { JsonWebToken.encode({ id: instructor.id }) } let!(:participant) { AssignmentParticipant.create!(user: s1, assignment_id: assignment.id, team: team, handle: 'handle') } let!(:participant2) { AssignmentParticipant.create!(user: s2, assignment_id: assignment.id, team: team, handle: 'handle') } @@ -324,5 +328,24 @@ end + describe '#edit_participant_scores' do + before do + allow(controller).to receive(:find_participant).with(participant.id.to_s).and_return(participant) + allow(controller).to receive(:list_questions).with(assignment).and_return(question) + allow(Response).to receive(:review_grades).with(participant, question).and_return([95, 90, 85]) # Example scores + end + + describe 'GET #edit_participant_scores' do + it 'renders the edit page and sets instance variables' do + request.headers['Authorization'] = "Bearer #{instructor_token}" + request.headers['Content-Type'] = 'application/json' + + get :edit_participant_scores, params: { id: participant.id } + expect(assigns(:participant)).to eq(participant) + expect(assigns(:assignment)).to eq(assignment) + expect(assigns(:scores)).to eq([95, 90, 85]) + end + end + end end From e3f6ae4e808d700c1dfade30cb450e477024d9bc Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 24 Mar 2025 13:09:14 -0400 Subject: [PATCH 30/41] Moved helper functions to grades_helper --- app/controllers/api/v1/grades_controller.rb | 133 +------------------- app/helpers/grades_helper.rb | 129 +++++++++++++++++++ 2 files changed, 130 insertions(+), 132 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 33c054a66..291951319 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -1,15 +1,6 @@ class Api::V1::GradesController < ApplicationController include GradesHelper - # Defines permissions for different actions based on user roles. - # 'view_my_scores' is allowed for students with specific permissions. - # 'view_team' is allowed for both students and TAs. - # 'view_grading_report' is allowed TAs and higher roles. - ACTION_PERMISSIONS = { - 'view_my_scores' => :student_with_permissions?, - 'view_team' => :student_or_ta? - }.freeze - def action_allowed permitted = check_permission(params[:requested_action]) render json: { allowed: permitted }, status: permitted ? :ok : :forbidden @@ -130,129 +121,7 @@ def instructor_review redirect_to_review(review_mapping) end - - - private - - # Private Methods associated with action_allowed?: - # Checks if the user has permission for the given action and executes the corresponding method. - def check_permission(action) - return has_privileges_of?('Teaching Assistant') unless ACTION_PERMISSIONS.key?(action) - - send(ACTION_PERMISSIONS[action]) - end - - # Checks if the student has the necessary permissions and authorizations to proceed. - def student_with_permissions? - has_role?('Student') && - self_review_finished? && - are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') - end - - # Checks if the user is either a student viewing their own team or has Teaching Assistant privileges. - def student_or_ta? - student_viewing_own_team? || has_privileges_of?('Teaching Assistant') - end - - # This method checks if the current user, who must have the 'Student' role, is viewing their own team. - def student_viewing_own_team? - return false unless has_role?('Student') - - participant = AssignmentParticipant.find_by(id: params[:id]) - participant && current_user_is_assignment_participant?(participant.assignment.id) - end - - # Check if the self-review for the participant is finished based on assignment settings and submission status. - def self_review_finished? - participant = Participant.find(params[:id]) - assignment = participant.try(:assignment) - self_review_enabled = assignment.try(:is_selfreview_enabled) - not_submitted = ResponseMap.self_review_pending?(participant.try(:id)) - if self_review_enabled - !not_submitted - else - true - end - end - - - # Private Methods associated with View methods: - # Determines if the rubric changes by round and returns the corresponding questions based on the criteria. - def filter_questionnaires(assignment) - questionnaires = assignment.questionnaires - if assignment.varying_rubrics_by_round? - retrieve_questions(questionnaires, assignment.id) - else - questions = {} - questionnaires.each do |questionnaire| - questions[questionnaire.id.to_s.to_sym] = questionnaire.questions - end - questions - end - end - - # Generates data for visualizing heat maps in the view statements. - def get_data_for_heat_map(assignment_id) - # Finds the assignment - @assignment = find_assignment(assignment_id) - # Extracts the questionnaires - @questions = filter_questionnaires(@assignment) - @scores = Response.review_grades(@assignment, @questions) - @review_score_count = @scores[:teams].length # After rejecting nil scores need original length to iterate over hash - @averages = Response.extract_team_averages(@scores[:teams]) - @avg_of_avg = Response.average_team_scores(@averages) - end - - # Private Method associated with edit_participant_scores: - # This method retrieves all questions from relevant questionnaires associated with this assignment. - def list_questions(assignment) - assignment.questionnaires.each_with_object({}) do |questionnaire, questions| - questions[questionnaire.id.to_s] = questionnaire.questions - end - end - - # Private Method associated with Update methods: - # Displays an error message if the participant is not found. - def handle_not_found - render json: { error: 'Participant not found.' }, status: :not_found - end - - # Checks if the participant's grade has changed compared to the new grade. - def grade_changed?(participant, new_grade) - return false if new_grade.nil? - - format('%.2f', params[:total_score]) != new_grade - end - - # Generates a message based on whether the participant's grade is present or computed. - def grade_message(participant) - participant.grade.nil? ? "The computed score will be used for #{participant.user.name}." : - "A score of #{participant.grade}% has been saved for #{participant.user.name}." - end - - - # Private Methods associated with instructor_review: - # Finds or creates a reviewer for the given user and assignment, and sets a handle if it's a new record - def find_or_create_reviewer(user_id, assignment_id) - reviewer = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) - reviewer.set_handle if reviewer.new_record? - reviewer - end - - # Finds or creates a review mapping between the reviewee and reviewer for the given assignment. - def find_or_create_review_mapping(reviewee_id, reviewer_id, assignment_id) - ReviewResponseMap.find_or_create_by(reviewee_id: reviewee_id, reviewer_id: reviewer_id, reviewed_object_id: assignment_id) - end - - # Redirects to the appropriate review page based on whether the review mapping is new or existing. - def redirect_to_review(review_mapping) - if review_mapping.new_record? - redirect_to controller: 'response', action: 'new', id: review_mapping.map_id, return: 'instructor' - else - review = Response.find_by(map_id: review_mapping.map_id) - redirect_to controller: 'response', action: 'edit', id: review.id, return: 'instructor' - end - end + end diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index fdef3696f..3592fae6f 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -1,6 +1,15 @@ module GradesHelper include PenaltyHelper + # Defines permissions for different actions based on user roles. + # 'view_my_scores' is allowed for students with specific permissions. + # 'view_team' is allowed for both students and TAs. + # 'view_grading_report' is allowed TAs and higher roles. + ACTION_PERMISSIONS = { + 'view_my_scores' => :student_with_permissions?, + 'view_team' => :student_or_ta? + }.freeze + # Calculates and applies penalties for participants of a given assignment. def penalties(assignment_id) @assignment = Assignment.find(assignment_id) @@ -145,6 +154,126 @@ def find_assignment(assignment_id) nil end + # Method associated with action_allowed?: + # Checks if the user has permission for the given action and executes the corresponding method. + def check_permission(action) + return has_privileges_of?('Teaching Assistant') unless ACTION_PERMISSIONS.key?(action) + + send(ACTION_PERMISSIONS[action]) + end + + # Checks if the student has the necessary permissions and authorizations to proceed. + def student_with_permissions? + has_role?('Student') && + self_review_finished? && + are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') + end + + # Checks if the user is either a student viewing their own team or has Teaching Assistant privileges. + def student_or_ta? + student_viewing_own_team? || has_privileges_of?('Teaching Assistant') + end + + # This method checks if the current user, who must have the 'Student' role, is viewing their own team. + def student_viewing_own_team? + return false unless has_role?('Student') + + participant = AssignmentParticipant.find_by(id: params[:id]) + participant && current_user_is_assignment_participant?(participant.assignment.id) + end + + # Check if the self-review for the participant is finished based on assignment settings and submission status. + def self_review_finished? + participant = Participant.find(params[:id]) + assignment = participant.try(:assignment) + self_review_enabled = assignment.try(:is_selfreview_enabled) + not_submitted = ResponseMap.self_review_pending?(participant.try(:id)) + if self_review_enabled + !not_submitted + else + true + end + end + + + # Methods associated with View methods: + # Determines if the rubric changes by round and returns the corresponding questions based on the criteria. + def filter_questionnaires(assignment) + questionnaires = assignment.questionnaires + if assignment.varying_rubrics_by_round? + retrieve_questions(questionnaires, assignment.id) + else + questions = {} + questionnaires.each do |questionnaire| + questions[questionnaire.id.to_s.to_sym] = questionnaire.questions + end + questions + end + end + + # Generates data for visualizing heat maps in the view statements. + def get_data_for_heat_map(assignment_id) + # Finds the assignment + @assignment = find_assignment(assignment_id) + # Extracts the questionnaires + @questions = filter_questionnaires(@assignment) + @scores = Response.review_grades(@assignment, @questions) + @review_score_count = @scores[:teams].length # After rejecting nil scores need original length to iterate over hash + @averages = Response.extract_team_averages(@scores[:teams]) + @avg_of_avg = Response.average_team_scores(@averages) + end + + # Method associated with edit_participant_scores: + # This method retrieves all questions from relevant questionnaires associated with this assignment. + def list_questions(assignment) + assignment.questionnaires.each_with_object({}) do |questionnaire, questions| + questions[questionnaire.id.to_s] = questionnaire.questions + end + end + + # Method associated with Update methods: + # Displays an error message if the participant is not found. + def handle_not_found + render json: { error: 'Participant not found.' }, status: :not_found + end + + # Checks if the participant's grade has changed compared to the new grade. + def grade_changed?(participant, new_grade) + return false if new_grade.nil? + + format('%.2f', params[:total_score]) != new_grade + end + + # Generates a message based on whether the participant's grade is present or computed. + def grade_message(participant) + participant.grade.nil? ? "The computed score will be used for #{participant.user.name}." : + "A score of #{participant.grade}% has been saved for #{participant.user.name}." + end + + + # Methods associated with instructor_review: + # Finds or creates a reviewer for the given user and assignment, and sets a handle if it's a new record + def find_or_create_reviewer(user_id, assignment_id) + reviewer = AssignmentParticipant.find_or_create_by(user_id: user_id, parent_id: assignment_id) + reviewer.set_handle if reviewer.new_record? + reviewer + end + + # Finds or creates a review mapping between the reviewee and reviewer for the given assignment. + def find_or_create_review_mapping(reviewee_id, reviewer_id, assignment_id) + ReviewResponseMap.find_or_create_by(reviewee_id: reviewee_id, reviewer_id: reviewer_id, reviewed_object_id: assignment_id) + end + + # Redirects to the appropriate review page based on whether the review mapping is new or existing. + def redirect_to_review(review_mapping) + if review_mapping.new_record? + redirect_to controller: 'response', action: 'new', id: review_mapping.map_id, return: 'instructor' + else + review = Response.find_by(map_id: review_mapping.map_id) + redirect_to controller: 'response', action: 'edit', id: review.id, return: 'instructor' + end + end + private # Determines if penalties should be calculated based on the assignment's penalty status. From 739b147b7eae6e3f918db9b5c1da9dec055c917d Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 24 Mar 2025 14:40:38 -0400 Subject: [PATCH 31/41] rspec for vie_team --- app/controllers/api/v1/grades_controller.rb | 8 ++-- app/helpers/grades_helper.rb | 4 +- app/helpers/penalty_helper.rb | 2 +- app/models/response.rb | 2 +- config/routes.rb | 1 + .../requests/api/v1/grades_controller_spec.rb | 45 +++++++++++++++++++ 6 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 291951319..419417b84 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -21,7 +21,8 @@ def view_grading_report # Additionally, it applies any penalties and determines the current stage of the assignment. # This method ensures participants have a comprehensive understanding of their scores and feedback def view_my_scores - fetch_participant_and_assignment + @participant = AssignmentParticipant.find(params[:id]) + @assignment = @participant.assignment @team_id = TeamsUser.team_id(@participant.parent_id, @participant.user_id) return if redirect_when_disallowed @@ -41,14 +42,15 @@ def view_my_scores # Additionally, it prepares the necessary data for displaying team-related information. # This method ensures participants have a clear understanding of their team's performance and any associated penalties. def view_team - fetch_participant_and_assignment + @participant = AssignmentParticipant.find(params[:id]) + @assignment = @participant.assignment @team = @participant.team @team_id = @team.id questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: nil).map(&:questionnaire) @questions = retrieve_questions(questionnaires, @assignment.id) @pscore = Response.participant_scores(@participant, @questions) - @penalties = calculate_penalty(@participant.id) + @penalties = get_penalty(@participant.id) @vmlist = process_questionare_for_team(@assignment, @team_id) @current_role_name = current_role_name diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index 3592fae6f..0dd68302d 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -56,8 +56,8 @@ def retrieve_questions(questionnaires, assignment_id) end # Retrieves the participant and their associated assignment data. - def fetch_participant_and_assignment - @participant = AssignmentParticipant.find(params[:id]) + def fetch_participant_and_assignment(id) + @participant = AssignmentParticipant.find(id) @assignment = @participant.assignment end diff --git a/app/helpers/penalty_helper.rb b/app/helpers/penalty_helper.rb index 7067f3f82..443ecdd46 100644 --- a/app/helpers/penalty_helper.rb +++ b/app/helpers/penalty_helper.rb @@ -1,5 +1,5 @@ module PenaltyHelper - def calculate_penalty(participant_id) + def get_penalty(participant_id) set_participant_and_assignment(participant_id) set_late_policy if @assignment.late_policy_id diff --git a/app/models/response.rb b/app/models/response.rb index aa694fc37..2f7a5b014 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -177,7 +177,7 @@ def self.review_grades(assignment, questions) end - def participant_scores(participant, questions) + def self.participant_scores(participant, questions) assignment = participant.assignment scores = {} scores[:participant] = participant diff --git a/config/routes.rb b/config/routes.rb index 39073a411..b4dae2a04 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,7 @@ post 'update_team' post 'update_participant_grade' get 'edit_participant_scores' + get 'view_team' end end end diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index 66022f2cc..b9251e49b 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -348,4 +348,49 @@ end end end + + describe '#view_team' do + let(:penalties) { { submission: 0, review: 0, meta_review: 0 } } + let(:participant_scores) { { total: 90 } } + let(:vmlist) { double('vmlist') } + let(:current_role_name) { 'Student' } + + before do + allow(controller).to receive(:fetch_participant_and_assignment).with(participant.id.to_s) do + controller.instance_variable_set(:@participant, participant) + controller.instance_variable_set(:@assignment, assignment) + end + allow(controller).to receive(:current_role_name).and_return(current_role_name) + + allow(AssignmentQuestionnaire).to receive(:where) + .with(assignment_id: assignment.id, topic_id: nil) + .and_return([assignment_questionnaire]) + + allow(controller).to receive(:retrieve_questions).and_return(question) + + # response_instance = instance_double(Response) + allow(Response).to receive(:participant_scores).with(participant, question).and_return(participant_scores) + + allow(controller).to receive(:get_penalty).with(participant.id).and_return(penalties) + allow(controller).to receive(:process_questionare_for_team).with(assignment, team.id).and_return(vmlist) + allow(controller).to receive(:instance_variable_set) + allow(controller).to receive(:instance_variable_get).and_call_original + end + + it 'assigns correct instance variables' do + + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + + get :view_team, params: { id: participant.id } + + expect(assigns(:team)).to eq(team) + expect(assigns(:team_id)).to eq(team.id) + expect(assigns(:questions)).to eq(question) + expect(assigns(:pscore)).to eq(participant_scores) + expect(assigns(:penalties)).to eq(penalties) + expect(assigns(:vmlist)).to eq(vmlist) + expect(assigns(:current_role_name)).to eq(current_role_name) + end + end end From 769d8fe48e1aa63eb1e839d9f25990f23bd99064 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 24 Mar 2025 17:09:02 -0400 Subject: [PATCH 32/41] added missing functions to assignment, signed_up_team, teams_user model, Added rspec for view_my_score --- app/controllers/api/v1/grades_controller.rb | 4 +- app/models/assignment.rb | 5 ++ app/models/signed_up_team.rb | 15 +++++ app/models/teams_user.rb | 17 ++++++ config/routes.rb | 1 + .../requests/api/v1/grades_controller_spec.rb | 55 +++++++++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 419417b84..1df0c040f 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -21,9 +21,9 @@ def view_grading_report # Additionally, it applies any penalties and determines the current stage of the assignment. # This method ensures participants have a comprehensive understanding of their scores and feedback def view_my_scores - @participant = AssignmentParticipant.find(params[:id]) + @participant = AssignmentParticipant.find(params[:id].to_i) @assignment = @participant.assignment - @team_id = TeamsUser.team_id(@participant.parent_id, @participant.user_id) + @team_id = TeamsUser.team_id(@participant.assignment_id, @participant.user_id) return if redirect_when_disallowed fetch_questionnaires_and_questions diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 6ba38d889..f65de394e 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -194,6 +194,11 @@ def varying_rubrics_by_round? rubric_with_round.present? end + def current_stage(topic_id = nil) + return 'Unknown' if staggered_and_no_topic?(topic_id) + due_date = find_current_stage(topic_id) + due_date.nil? || due_date == 'Finished' ? 'Finished' : DeadlineType.find(due_date.deadline_type_id).name + end end \ No newline at end of file diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 39b1e2de4..73c5fd43f 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -1,4 +1,19 @@ class SignedUpTeam < ApplicationRecord belongs_to :sign_up_topic belongs_to :team + + def self.topic_id(assignment_id, user_id) + # team_id variable represents the team_id for this user in this assignment + team_id = TeamsUser.team_id(assignment_id, user_id) + topic_id_by_team_id(team_id) if team_id + end + + def self.topic_id_by_team_id(team_id) + signed_up_teams = SignedUpTeam.where(team_id: team_id, is_waitlisted: 0) + if signed_up_teams.blank? + nil + else + signed_up_teams.first.topic_id + end + end end diff --git a/app/models/teams_user.rb b/app/models/teams_user.rb index 9e1768b94..fa6c9eb93 100644 --- a/app/models/teams_user.rb +++ b/app/models/teams_user.rb @@ -20,4 +20,21 @@ def self.remove_team(user_id, team_id) team_user&.destroy end + def self.team_id(assignment_id, user_id) + # team_id variable represents the team_id for this user in this assignment + team_id = nil + teams_users = TeamsUser.where(user_id: user_id) + teams_users.each do |teams_user| + if teams_user.team_id == nil + next + end + team = Team.find(teams_user.team_id) + if team.parent_id == assignment_id + team_id = teams_user.team_id + break + end + end + team_id + end + end diff --git a/config/routes.rb b/config/routes.rb index b4dae2a04..8fc385c6c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -130,6 +130,7 @@ post 'update_participant_grade' get 'edit_participant_scores' get 'view_team' + get 'view_my_scores' end end end diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index b9251e49b..a040f990c 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -393,4 +393,59 @@ expect(assigns(:current_role_name)).to eq(current_role_name) end end + + describe '#view_my_scores' do + let(:team_id) { 16 } + let(:topic_id) { 16 } + let(:stage) { instance_double('Current Stage') } + let(:penalty) { { submission: 0, review: 0, meta_review: 0 } } + let!(:participant2) { AssignmentParticipant.create!(user: s2, assignment_id: assignment.id, team: team, handle: 'handle') } + + before do + allow(AssignmentParticipant).to receive(:find).with(participant2.id).and_return(participant2) + allow(participant2).to receive(:assignment).and_return(assignment) + allow(TeamsUser).to receive(:team_id).with(anything, anything).and_return(16) + allow(SignedUpTeam).to receive(:topic_id).with(anything, anything).and_return(16) + allow(participant2).to receive_message_chain(:assignment, :current_stage).and_return(stage) + + # Mock the methods being called in view_my_scores + allow(controller).to receive(:redirect_when_disallowed).and_return(false) # Assuming no redirect + allow(controller).to receive(:fetch_questionnaires_and_questions) + allow(controller).to receive(:fetch_participant_scores) + allow(controller).to receive(:update_penalties) + allow(controller).to receive(:fetch_feedback_summary) + end + + it 'sets up participant and assignment variables' do + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + + get :view_my_scores, params: { id: participant2.id } + + expect(assigns(:assignment)).to eq(assignment) + end + + it 'sets up team_id, topic_id, and stage' do + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + + get :view_my_scores, params: { id: participant2.id } + + expect(assigns(:team_id)).to eq(team_id) + expect(assigns(:topic_id)).to eq(topic_id) + expect(assigns(:stage)).to eq(stage) + end + + it 'fetches questionnaires and questions, participant scores, and feedback summary' do + request.headers['Authorization'] = "Bearer #{student_token}" + request.headers['Content-Type'] = 'application/json' + + get :view_my_scores, params: { id: participant2.id } + + expect(controller).to have_received(:fetch_questionnaires_and_questions) + expect(controller).to have_received(:fetch_participant_scores) + expect(controller).to have_received(:fetch_feedback_summary) + end + end + end From 9d97168e308f95cdaed760f68403b6fc171d3ea2 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 24 Mar 2025 18:49:17 -0400 Subject: [PATCH 33/41] Rspec for view_grading_reprort --- app/controllers/api/v1/grades_controller.rb | 2 +- app/models/response.rb | 9 +++++ .../requests/api/v1/grades_controller_spec.rb | 35 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 1df0c040f..089aaca87 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -11,7 +11,7 @@ def action_allowed # Additionally, it provides a final score, which is the average of all reviews, and highlights the greatest # difference in scores among the reviews. def view_grading_report - get_data_for_heat_map(params[:id]) + get_data_for_heat_map(params[:id].to_i) update_penalties @show_reputation = false end diff --git a/app/models/response.rb b/app/models/response.rb index 2f7a5b014..dd6519c80 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -323,6 +323,15 @@ def compute_avg_and_ranges_hash(assignment) scores end + def self.extract_team_averages(scores) + scores[:teams].reject! { |_k, v| v[:scores][:avg].nil? } + scores[:teams].map { |_k, v| v[:scores][:avg].to_i } + end + + def self.average_team_scores(array) + array.inject(0) { |sum, x| sum + x } / array.size.to_f + end + private # Get all of the questions asked during peer review for the given team's work diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index a040f990c..ad3f32098 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -448,4 +448,39 @@ end end + + describe '#view_grading_report' do + let(:scores) { { teams: [double('score1'), double('score2')] } } + let(:averages) { [double('average1'), double('average2')] } + + before do + allow(controller).to receive(:find_assignment).and_return(assignment) + allow(controller).to receive(:filter_questionnaires).with(assignment).and_return(question) + allow(Response).to receive(:review_grades).and_return(scores) + allow(Response).to receive(:extract_team_averages).and_return(averages) + allow(Response).to receive(:average_team_scores).and_return(5.0) + allow(controller).to receive(:penalties).and_return(nil) + + allow(controller).to receive(:get_data_for_heat_map).and_call_original + allow(controller).to receive(:update_penalties).and_call_original + + end + it 'sets @assignment, @questions, @scores, @review_score_count, @averages, and @avg_of_avg' do + request.headers['Authorization'] = "Bearer #{ta_token}" + request.headers['Content-Type'] = 'application/json' + get :view_grading_report, params: { id: assignment.id } + + expect(controller).to have_received(:get_data_for_heat_map).with(assignment.id) + expect(controller).to have_received(:update_penalties) + + expect(assigns(:assignment)).to eq(assignment) + expect(assigns(:scores)).to eq(scores) + expect(assigns(:review_score_count)).to eq(2) # Since there are 2 teams in scores + expect(assigns(:averages)).to eq(averages) + expect(assigns(:avg_of_avg)).to eq(5.0) + + # Check the @show_reputation instance variable + expect(assigns(:show_reputation)).to be false + end + end end From 347a692a812ec596c600e121d8d7d22ee100d316 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Fri, 18 Apr 2025 12:19:00 -0400 Subject: [PATCH 34/41] Dummy data and Working API for view_grading_report, view_team, view_my_scores --- app/controllers/api/v1/grades_controller.rb | 79 +++-- app/helpers/grades_helper.rb | 137 +++++---- app/helpers/summary_helper.rb | 143 +++++++++ .../analytic/assignment_team_analytic.rb | 108 +++++++ app/models/answer.rb | 33 ++ app/models/assignment.rb | 7 + app/models/assignment_team.rb | 290 ++++++++++++++++++ app/models/choice_question.rb | 2 + app/models/due_date.rb | 21 ++ app/models/questionnaire.rb | 220 ++++++++++--- app/models/questionnaire_header.rb | 34 ++ app/models/response.rb | 48 +-- app/models/review_questionnaire.rb | 43 +++ app/models/review_response_map.rb | 14 + app/models/score_view.rb | 13 + app/models/scored_question.rb | 18 ++ app/models/signed_up_team.rb | 4 +- app/models/tag_prompt.rb | 81 +++++ app/models/tag_prompt_deployment.rb | 80 +++++ app/models/teams_user.rb | 2 +- app/models/vm_question_response.rb | 197 ++++++++++++ app/models/vm_question_response_row.rb | 44 +++ app/models/vm_question_response_score_cell.rb | 18 ++ config/initializers/load_config.rb | 1 + config/webservices.yml | 8 + .../20250411154148_add_name_to_teams.rb | 5 + ...0250413164318_add_type_to_response_maps.rb | 5 + ..._add_round_and_version_num_to_responses.rb | 6 + .../20250414180932_add_type_to_questions.rb | 5 + .../20250414182039_create_score_views.rb | 27 ++ ...250415010637_remove_type_from_questions.rb | 5 + ...ire_weight_to_assignment_questionnaires.rb | 5 + ...418051410_create_tag_prompt_assignments.rb | 17 + ...t_assignments_to_tag_prompt_deployments.rb | 5 + .../20250418051819_create_task_prompts.rb | 11 + ...2239_rename_task_prompts_to_tag_prompts.rb | 5 + db/schema.rb | 52 +++- 37 files changed, 1620 insertions(+), 173 deletions(-) create mode 100644 app/helpers/summary_helper.rb create mode 100644 app/models/analytic/assignment_team_analytic.rb create mode 100644 app/models/assignment_team.rb create mode 100644 app/models/choice_question.rb create mode 100644 app/models/questionnaire_header.rb create mode 100644 app/models/review_questionnaire.rb create mode 100644 app/models/score_view.rb create mode 100644 app/models/scored_question.rb create mode 100644 app/models/tag_prompt.rb create mode 100644 app/models/tag_prompt_deployment.rb create mode 100644 app/models/vm_question_response.rb create mode 100644 app/models/vm_question_response_row.rb create mode 100644 app/models/vm_question_response_score_cell.rb create mode 100644 config/initializers/load_config.rb create mode 100644 config/webservices.yml create mode 100644 db/migrate/20250411154148_add_name_to_teams.rb create mode 100644 db/migrate/20250413164318_add_type_to_response_maps.rb create mode 100644 db/migrate/20250413170704_add_round_and_version_num_to_responses.rb create mode 100644 db/migrate/20250414180932_add_type_to_questions.rb create mode 100644 db/migrate/20250414182039_create_score_views.rb create mode 100644 db/migrate/20250415010637_remove_type_from_questions.rb create mode 100644 db/migrate/20250415025716_add_questionnaire_weight_to_assignment_questionnaires.rb create mode 100644 db/migrate/20250418051410_create_tag_prompt_assignments.rb create mode 100644 db/migrate/20250418051607_rename_tag_prompt_assignments_to_tag_prompt_deployments.rb create mode 100644 db/migrate/20250418051819_create_task_prompts.rb create mode 100644 db/migrate/20250418052239_rename_task_prompts_to_tag_prompts.rb diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 089aaca87..7c3126838 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -11,9 +11,10 @@ def action_allowed # Additionally, it provides a final score, which is the average of all reviews, and highlights the greatest # difference in scores among the reviews. def view_grading_report - get_data_for_heat_map(params[:id].to_i) - update_penalties - @show_reputation = false + assignment_id = params[:id].to_i + data = get_data_for_heat_map(assignment_id) + + render json: data, status: :ok end # The view_my_scores method provides participants with a detailed overview of their performance in an assignment. @@ -21,39 +22,65 @@ def view_grading_report # Additionally, it applies any penalties and determines the current stage of the assignment. # This method ensures participants have a comprehensive understanding of their scores and feedback def view_my_scores - @participant = AssignmentParticipant.find(params[:id].to_i) - @assignment = @participant.assignment - @team_id = TeamsUser.team_id(@participant.assignment_id, @participant.user_id) - return if redirect_when_disallowed + participant = AssignmentParticipant.find(params[:id].to_i) + assignment = participant.assignment + team_id = TeamsUser.team_id(participant.assignment_id, participant.user_id) - fetch_questionnaires_and_questions - fetch_participant_scores + # return if redirect_when_disallowed(participant) - @topic_id = SignedUpTeam.topic_id(@participant.assignment.id, @participant.user_id) - @stage = @participant.assignment.current_stage(@topic_id) - update_penalties - - # prepare feedback summaries - fetch_feedback_summary + questions = fetch_questionnaires_and_questions(assignment) + + pscore = fetch_participant_scores(participant, questions) + + topic_id = SignedUpTeam.find_topic_id_for_user(participant.assignment.id, participant.user_id) + stage = participant.assignment.current_stage(topic_id) + + # all_penalties = update_penalties(assignment) + + # Feedback Summary needs to be checked once + # summary_ws_url = WEBSERVICE_CONFIG['summary_webservice_url'] + sum = SummaryHelper::Summary.new.summarize_reviews_by_reviewee(questions, assignment, team_id, 'http://peerlogic.csc.ncsu.edu/sum/v1.0/summary/8/lsa', session) + + render json: { + participant: participant, + assignment: assignment, + team_id: team_id, + topic_id: topic_id, + stage: stage, + questions: questions, + pscore: pscore, + summary: sum.summary, + avg_scores_by_round: sum.avg_scores_by_round, + avg_scores_by_criterion: sum.avg_scores_by_criterion + } end # The view_team method provides an alternative view for participants, focusing on team performance. # It retrieves the participant, assignment, and team information, and calculated scores and penalties. # Additionally, it prepares the necessary data for displaying team-related information. # This method ensures participants have a clear understanding of their team's performance and any associated penalties. - def view_team - @participant = AssignmentParticipant.find(params[:id]) - @assignment = @participant.assignment - @team = @participant.team - @team_id = @team.id - questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: nil).map(&:questionnaire) - @questions = retrieve_questions(questionnaires, @assignment.id) - @pscore = Response.participant_scores(@participant, @questions) - @penalties = get_penalty(@participant.id) - @vmlist = process_questionare_for_team(@assignment, @team_id) - @current_role_name = current_role_name + def view_team + participant = AssignmentParticipant.find(params[:id]) + assignment = participant.assignment + team = participant.team + team_id = team.id + + questionnaires = AssignmentQuestionnaire.where(assignment_id: assignment.id).map(&:questionnaire) + questions = retrieve_questions(questionnaires, assignment.id) + pscore = Response.participant_scores(participant, questions) + vmlist = process_questionare_for_team(assignment, team_id,questionnaires, team, participant) + + render json: { + participant: participant, + assignment: assignment, + team: team, + team_id: team_id, + questions: questions, + pscore: pscore, + vmlist: vmlist + } end diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index 0dd68302d..c3815bb80 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -12,27 +12,28 @@ module GradesHelper # Calculates and applies penalties for participants of a given assignment. def penalties(assignment_id) - @assignment = Assignment.find(assignment_id) - calculate_for_participants = should_calculate_penalties? + assignment = Assignment.find(assignment_id) + calculate_for_participants = should_calculate_penalties?(assignment) - Participant.where(parent_id: assignment_id).each do |participant| + Participant.where(assignment_id: assignment_id).each do |participant| penalties = calculate_penalty(participant.id) - @total_penalty = calculate_total_penalty(penalties) + total_penalty = calculate_total_penalty(penalties) - if @total_penalty > 0 - @total_penalty = apply_max_penalty(@total_penalty) - attributes(@participant) if calculate_for_participants + if total_penalty > 0 + total_penalty = apply_max_penalty(total_penalty) + attributes(participant) if calculate_for_participants end - assign_all_penalties(participant, penalties) + all_penalties = assign_all_penalties(participant, penalties) end - mark_penalty_as_calculated unless @assignment.is_penalty_calculated + mark_penalty_as_calculated(assignment) unless assignment.is_penalty_calculated + return all_penalties end # Calculates and applies penalties for the current assignment. - def update_penalties - penalties(@assignment.id) + def update_penalties(assignment) + penalties(assignment.id) end # Retrieves the name of the current user's role, if available. @@ -50,9 +51,10 @@ def retrieve_questions(questionnaires, assignment_id) else (questionnaire.symbol.to_s + round.to_s).to_sym end + # questionnaire_symbol = questionnaire.id questions[questionnaire_symbol] = questionnaire.questions end - questions + questions end # Retrieves the participant and their associated assignment data. @@ -62,14 +64,16 @@ def fetch_participant_and_assignment(id) end # Retrieves the questionnaires and their associated questions for the assignment. - def fetch_questionnaires_and_questions - questionnaires = @assignment.questionnaires - @questions = retrieve_questions(questionnaires, @assignment.id) + def fetch_questionnaires_and_questions(assignment) + questionnaires = assignment.questionnaires + questions = retrieve_questions(questionnaires, assignment.id) + return questions end # Fetches the scores for the participant based on the retrieved questions. - def fetch_participant_scores - @pscore = participant_scores(@participant, @questions) + def fetch_participant_scores(participant, questions) + pscore = Response.participant_scores(participant, questions) + return pscore end @@ -83,43 +87,43 @@ def fetch_feedback_summary end # Processes questionnaires for a team, considering topic-specific and round-specific rubrics, and populates view models accordingly. - def process_questionare_for_team(assignment, team_id) + def process_questionare_for_team(assignment, team_id, questionnaires, team, participant) vmlist = [] counter_for_same_rubric = 0 - if @assignment.vary_by_topic? - topic_id = SignedUpTeam.topic_id_by_team_id(@team_id) - topic_specific_questionnaire = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: topic_id).first.questionnaire - @vmlist << populate_view_model(topic_specific_questionnaire) - end + # if @assignment.vary_by_topic? + # topic_id = SignedUpTeam.topic_id_by_team_id(@team_id) + # topic_specific_questionnaire = AssignmentQuestionnaire.where(assignment_id: @assignment.id, topic_id: topic_id).first.questionnaire + # @vmlist << populate_view_model(topic_specific_questionnaire) + # end questionnaires.each do |questionnaire| - @round = nil + round = nil # Guard clause to skip questionnaires that have already been populated for topic specific reviewing - if @assignment.vary_by_topic? && questionnaire.type == 'ReviewQuestionnaire' - next # Assignments with topic specific rubrics cannot have multiple rounds of review - end + # if @assignment.vary_by_topic? && questionnaire.type == 'ReviewQuestionnaire' + # next # Assignments with topic specific rubrics cannot have multiple rounds of review + # end - if @assignment.varying_rubrics_by_round? && questionnaire.type == 'ReviewQuestionnaire' - questionnaires = AssignmentQuestionnaire.where(assignment_id: @assignment.id, questionnaire_id: questionnaire.id) + if assignment.varying_rubrics_by_round? && questionnaire.questionnaire_type == 'ReviewQuestionnaire' + questionnaires = AssignmentQuestionnaire.where(assignment_id: assignment.id, questionnaire_id: questionnaire.id) if questionnaires.count > 1 - @round = questionnaires[counter_for_same_rubric].used_in_round + round = questionnaires[counter_for_same_rubric].used_in_round counter_for_same_rubric += 1 else - @round = questionnaires[0].used_in_round + round = questionnaires[0].used_in_round counter_for_same_rubric = 0 end end - vmlist << populate_view_model(questionnaire) + vmlist << populate_view_model(questionnaire, assignment, round, team, participant) end return vmlist end # Redirects the user if they are not allowed to access the assignment, based on team or reviewer authorization. - def redirect_when_disallowed - if team_assignment? - redirect_if_not_on_correct_team + def redirect_when_disallowed(participant) + if is_team_assignment?(participant) + redirect_if_not_on_correct_team(participant) else redirect_if_not_authorized_reviewer end @@ -127,13 +131,13 @@ def redirect_when_disallowed end # Populates the view model with questionnaire data, team members, reviews, and calculated metrics. - def populate_view_model(questionnaire) - vm = VmQuestionResponse.new(questionnaire, @assignment, @round) + def populate_view_model(questionnaire, assignment, round, team, participant) + vm = VmQuestionResponse.new(questionnaire, assignment, round) vmquestions = questionnaire.questions vm.add_questions(vmquestions) - vm.add_team_members(@team) - qn = AssignmentQuestionnaire.where(assignment_id: @assignment.id, used_in_round: 2).size >= 1 - vm.add_reviews(@participant, @team, @assignment.varying_rubrics_by_round?) + vm.add_team_members(team) + qn = AssignmentQuestionnaire.where(assignment_id: assignment.id, used_in_round: 2).size >= 1 + vm.add_reviews(participant, team, assignment.varying_rubrics_by_round?) vm.calculate_metrics vm end @@ -213,14 +217,23 @@ def filter_questionnaires(assignment) # Generates data for visualizing heat maps in the view statements. def get_data_for_heat_map(assignment_id) - # Finds the assignment - @assignment = find_assignment(assignment_id) - # Extracts the questionnaires - @questions = filter_questionnaires(@assignment) - @scores = Response.review_grades(@assignment, @questions) - @review_score_count = @scores[:teams].length # After rejecting nil scores need original length to iterate over hash - @averages = Response.extract_team_averages(@scores[:teams]) - @avg_of_avg = Response.average_team_scores(@averages) + assignment = find_assignment(assignment_id) + questions = filter_questionnaires(assignment) + scores = Response.review_grades(assignment, questions) + review_score_count = scores[:teams].length # After rejecting nil scores need original length to iterate over hash + averages = Response.extract_team_averages(scores[:teams]) + avg_of_avg = Response.average_team_scores(averages) + + # Construct the data as a JSON object + { + assignment: assignment, + questions: questions, + scores: scores, + review_score_count: review_score_count, + averages: averages, + avg_of_avg: avg_of_avg, + show_reputation: false + } end # Method associated with edit_participant_scores: @@ -277,8 +290,8 @@ def redirect_to_review(review_mapping) private # Determines if penalties should be calculated based on the assignment's penalty status. - def should_calculate_penalties? - !@assignment.is_penalty_calculated + def should_calculate_penalties?(assignment) + !assignment.is_penalty_calculated end # Calculates the total penalty from submission, review, and meta-review penalties. @@ -294,27 +307,29 @@ def apply_max_penalty(total_penalty) end # Marks the assignment's penalty status as calculated. - def mark_penalty_as_calculated - @assignment.update(is_penalty_calculated: true) + def mark_penalty_as_calculated(assignment) + assignment.update(is_penalty_calculated: true) end def assign_all_penalties(participant, penalties) - @all_penalties[participant.id] = { + all_penalties[participant.id] = { submission: penalties[:submission], review: penalties[:review], meta_review: penalties[:meta_review], total_penalty: @total_penalty } + return all_penalties end # Checks if the assignment is a team assignment based on the maximum team size. - def team_assignment? - @participant.assignment.max_team_size > 1 + def is_team_assignment?(participant) + participant.assignment.max_team_size > 1 end # Redirects the user if they are not on the correct team that provided the feedback. - def redirect_if_not_on_correct_team - team = @participant.team + def redirect_if_not_on_correct_team(participant) + team = participant.team + puts team.attributes if team.nil? || !team.user?(session[:user]) flash[:error] = 'You are not on the team that wrote this feedback' redirect_to '/' @@ -322,13 +337,15 @@ def redirect_if_not_on_correct_team end # Redirects the user if they are not an authorized reviewer for the feedback. - def redirect_if_not_authorized_reviewer - reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: @participant.assignment.id).first + def redirect_if_not_authorized_reviewer(participant) + reviewer = AssignmentParticipant.where(user_id: session[:user].id, parent_id: participant.assignment.id).first return if current_user_id?(reviewer.try(:user_id)) flash[:error] = 'You are not authorized to view this feedback' redirect_to '/' end - + # def get_penalty_from_helper(participant_id): + # get_penalty(participant_id) + # end end \ No newline at end of file diff --git a/app/helpers/summary_helper.rb b/app/helpers/summary_helper.rb new file mode 100644 index 000000000..091bb3b0b --- /dev/null +++ b/app/helpers/summary_helper.rb @@ -0,0 +1,143 @@ +# require for webservice calls +require 'json' +require 'rest_client' +require 'logger' + +# required by autosummary +module SummaryHelper + class Summary + attr_accessor :summary, :reviewers, :avg_scores_by_reviewee, :avg_scores_by_round, :avg_scores_by_criterion, :summary_ws_url + + def summarize_reviews_by_reviewee(questions, assignment, reviewee_id, summary_ws_url, _session = nil) + self.summary = ({}) + self.avg_scores_by_round = ({}) + self.avg_scores_by_criterion = ({}) + self.summary_ws_url = summary_ws_url + + # get all answers for each question and send them to summarization WS + questions.each_with_index do |question, index| + round = index + 1 + summary[round.to_s] = {} + avg_scores_by_criterion[round.to_s] = {} + avg_scores_by_round[round.to_s] = 0.0 + + question_iterator = nil + if question[1] == nil + question_iterator = [*question] + else + question_iterator = question[1] + end + + question_iterator.each do |question| + next if question.question_type.eql?('SectionHeader') + + summarize_reviews_by_reviewee_question(assignment, reviewee_id, question, round) + avg_scores_by_round[round.to_s] = calculate_avg_score_by_round(avg_scores_by_criterion[round.to_s], questions[round]) + end + end + self + end + + # get average scores and summary for each question in a review by a reviewer + def summarize_reviews_by_reviewee_question(assignment, reviewee_id, question, round) + question_answers = Answer.answers_by_question_for_reviewee(assignment.id, reviewee_id, question.id) + + avg_scores_by_criterion[round.to_s][question.txt] = calculate_avg_score_by_criterion(question_answers, get_max_score_for_question(question)) + + # summary[round.to_s][question.txt] = summarize_sentences(break_up_comments_to_sentences(question_answers), summary_ws_url) + summary[round.to_s][question.txt] = break_up_comments_to_sentences(question_answers) + + end + + def get_max_score_for_question(question) + question.question_type.eql?('Checkbox') ? 1 : Questionnaire.where(id: question.questionnaire_id).first.max_question_score + end + + def summarize_sentences(comments, summary_ws_url) + logger = Logger.new(STDOUT) + logger.level = Logger::WARN + param = { sentences: comments } + # call web service + begin + # sum_json = RestClient.post summary_ws_url, param.to_json, content_type: :json, accept: :json + sum_json = RestClient::Request.execute( + method: :post, + url: summary_ws_url, + payload: param.to_json, + headers: { content_type: :json, accept: :json }, + open_timeout: 5, # seconds to wait for connection to open + read_timeout: 10 # seconds to wait for response + ) + # store each summary in a hashmap and use the question as the key + summary = JSON.parse(sum_json)['summary'] + ps = PragmaticSegmenter::Segmenter.new(text: summary) + return ps.segment + rescue StandardError => e + logger.warn "Standard Error: #{e.inspect}" + return ['Problem with WebServices', 'Please contact the Expertiza Development team'] + end + end + + # convert answers to each question to sentences + def get_sentences(answer) + sentences = answer.comments.gsub!(/[.?!]/, '\1|').try(:split, '|') || nil unless answer.nil? || answer.comments.nil? + sentences.map!(&:strip) unless sentences.nil? + sentences + end + + def break_up_comments_to_sentences(question_answers) + # store answers of each question in an array to be converted into json + comments = [] + question_answers.each do |answer| + sentences = get_sentences(answer) + # add the comment to an array to be converted as a json request + comments.concat(sentences) unless sentences.nil? + end + comments + end + + def calculate_avg_score_by_criterion(question_answers, q_max_score) + # get score and summary of answers for each question + # only include divide the valid_answer_sum with the number of valid answers + + valid_answer_counter = 0 + question_score = 0.0 + question_answers.each do |question_answer| + # calculate score per question + unless question_answer.answer.nil? + question_score += question_answer.answer + valid_answer_counter += 1 + end + end + + if (valid_answer_counter > 0) && (q_max_score > 0) + # convert the score in percentage + question_score /= (valid_answer_counter * q_max_score) + question_score = question_score.round(2) * 100 + end + + question_score + end + + def calculate_round_score(avg_scores_by_criterion, criterions) + round_score = sum_weight = 0.0 + # include this score in the average round score if the weight is valid & q is criterion + criterions = [*criterions] + criterions.each do |criteria| + if !criteria.weight.nil? && (criteria.weight > 0) && criteria.type.eql?('Criterion') + round_score += avg_scores_by_criterion.values.first * criteria.weight + sum_weight += criteria.weight + end + end + round_score /= sum_weight if (sum_weight > 0) && (round_score > 0) + round_score + end + + def calculate_avg_score_by_round(avg_scores_by_criterion, criterions) + round_score = calculate_round_score(avg_scores_by_criterion, criterions) + round_score.round(2) + end + end +end + +# end required by autosummary diff --git a/app/models/analytic/assignment_team_analytic.rb b/app/models/analytic/assignment_team_analytic.rb new file mode 100644 index 000000000..a2c47b925 --- /dev/null +++ b/app/models/analytic/assignment_team_analytic.rb @@ -0,0 +1,108 @@ +# require 'analytic/response_analytic' +module AssignmentTeamAnalytic + #======= general ==========# + def num_participants + participants.count + end + + def num_reviews + responses.count + end + + #========== score ========# + def average_review_score + if num_reviews == 0 + 0 + else + review_scores.inject(:+).to_f / num_reviews + end + end + + def max_review_score + review_scores.max + end + + def min_review_score + review_scores.min + end + + #======= word count =======# + def total_review_word_count + review_word_counts.inject(:+) + end + + def average_review_word_count + if num_reviews == 0 + 0 + else + total_review_word_count.to_f / num_reviews + end + end + + def max_review_word_count + review_word_counts.max + end + + def min_review_word_count + review_word_counts.min + end + + #===== character count ====# + def total_review_character_count + review_character_counts.inject(:+) + end + + def average_review_character_count + if num_reviews == 0 + 0 + else + total_review_character_count.to_f / num_reviews + end + end + + def max_review_character_count + review_character_counts.max + end + + def min_review_character_count + review_character_counts.min + end + + def review_character_counts + list = [] + responses.each do |response| + list << response.total_character_count + end + if list.empty? + [0] + else + list + end + end + + # return an array containing the score of all the reviews + def review_scores + list = [] + responses.each do |response| + list << response.average_score + end + if list.empty? + [0] + else + list + end + end + + def review_word_counts + list = [] + responses.each do |response| + list << response.total_word_count + end + if list.empty? + [0] + else + list + end + end + end + \ No newline at end of file diff --git a/app/models/answer.rb b/app/models/answer.rb index 2b55378fd..31ff9168b 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,4 +1,37 @@ class Answer < ApplicationRecord belongs_to :response belongs_to :item + + def self.answers_by_question_for_reviewee_in_round(assignment_id, reviewee_id, q_id, round) + # get all answers to this question + question_answer = Answer.select(:answer, :comments) + .joins('join responses on responses.id = answers.response_id') + .joins('join response_maps on responses.map_id = response_maps.id') + .joins('join questions on questions.id = answers.question_id') + .where("response_maps.reviewed_object_id = ? and + response_maps.reviewee_id = ? and + answers.question_id = ? and + responses.round = ?", assignment_id, reviewee_id, q_id, round) + question_answer + end + + def self.answers_by_question(assignment_id, q_id) + question_answer = Answer.select('DISTINCT answers.comments, answers.answer') + .joins('JOIN questions ON answers.question_id = questions.id') + .joins('JOIN responses ON responses.id = answers.response_id') + .joins('JOIN response_maps ON responses.map_id = response_maps.id') + .where('answers.question_id = ? and response_maps.reviewed_object_id = ?', q_id, assignment_id) + question_answer + end + + def self.answers_by_question_for_reviewee(assignment_id, reviewee_id, q_id) + question_answers = Answer.select(:answer, :comments) + .joins('join responses on responses.id = answers.response_id') + .joins('join response_maps on responses.map_id = response_maps.id') + .joins('join questions on questions.id = answers.question_id') + .where("response_maps.reviewed_object_id = ? and + response_maps.reviewee_id = ? and + answers.question_id = ? ", assignment_id, reviewee_id, q_id) + question_answers + end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index f65de394e..5509d5acf 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -201,4 +201,11 @@ def current_stage(topic_id = nil) due_date.nil? || due_date == 'Finished' ? 'Finished' : DeadlineType.find(due_date.deadline_type_id).name end + def find_current_stage(topic_id = nil) + next_due_date = DueDate.get_next_due_date(id, topic_id) + return 'Finished' if next_due_date.nil? + + next_due_date + end + end \ No newline at end of file diff --git a/app/models/assignment_team.rb b/app/models/assignment_team.rb new file mode 100644 index 000000000..cf07c1296 --- /dev/null +++ b/app/models/assignment_team.rb @@ -0,0 +1,290 @@ +class AssignmentTeam < Team + require File.dirname(__FILE__) + '/analytic/assignment_team_analytic' + include AssignmentTeamAnalytic + # include Scoring + + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'parent_id' + has_many :review_mappings, class_name: 'ReviewResponseMap', foreign_key: 'reviewee_id' + has_many :review_response_maps, foreign_key: 'reviewee_id' + has_many :responses, through: :review_response_maps, foreign_key: 'map_id' + # START of contributor methods, shared with AssignmentParticipant + + # Added for E1973, Team reviews. + # Some methods prompt a reviewer for a user id. This method just returns the user id of the first user in the team + # This is a very hacky way to deal with very complex functionality but the reasoning is this: + # The reason this is being added is to give ReviewAssignment#reject_own_submission a way to reject the submission + # Of the reviewer. If there are team reviews, there must be team submissions, so any team member's user id will do. + # Hopefully, this logic applies if there are other situations where reviewer.user_id was called + # EDIT: A situation was found which differs slightly. If the current user is on the team, we want to + # return that instead for instances where the code uses the current user. + def user_id + @current_user.id if !@current_user.nil? && users.include?(@current_user) + users.first.id + end + + # E1973 + # stores the current user so that we can check them when returning the user_id + def set_current_user(current_user) + @current_user = current_user + end + + # Whether this team includes a given participant or not + def includes?(participant) + participants.include?(participant) + end + + # Get the parent of this class=>Assignment + def parent_model + 'Assignment' + end + + def self.parent_model(id) + Assignment.find(id) + end + + # Get the name of the class + def fullname + name + end + + # Get the review response map + def review_map_type + 'ReviewResponseMap' + end + + # Prototype method to implement prototype pattern + def self.prototype + AssignmentTeam.new + end + + # Use current object (AssignmentTeam) as reviewee and create the ReviewResponseMap record + def assign_reviewer(reviewer) + assignment = Assignment.find(parent_id) + raise 'The assignment cannot be found.' if assignment.nil? + + ReviewResponseMap.create(reviewee_id: id, reviewer_id: reviewer.get_reviewer.id, reviewed_object_id: assignment.id, team_reviewing_enabled: assignment.team_reviewing_enabled) + end + + # E-1973 If a team is being treated as a reviewer of an assignment, then they are the reviewer + def get_reviewer + self + end + + # Evaluates whether any contribution by this team was reviewed by reviewer + # @param[in] reviewer AssignmentParticipant object + def reviewed_by?(reviewer) + ReviewResponseMap.where('reviewee_id = ? && reviewer_id = ? && reviewed_object_id = ?', id, reviewer.get_reviewer.id, assignment.id).count > 0 + end + + # Topic picked by the team for the assignment + # This method needs refactoring: it sounds like it returns a topic object but in fact it returns an id + def topic + SignedUpTeam.find_by(team_id: id, is_waitlisted: 0).try(:topic_id) + end + + # Whether the team has submitted work or not + def has_submissions? + submitted_files.any? || submitted_hyperlinks.present? + end + + # Get Participants of the team + def participants + users = self.users + participants = [] + users.each do |user| + participant = AssignmentParticipant.find_by(user_id: user.id, parent_id: parent_id) + participants << participant unless participant.nil? + end + participants + end + alias get_participants participants + + # Delete the team + def delete + if self[:type] == 'AssignmentTeam' + sign_up = SignedUpTeam.find_team_participants(parent_id.to_s).select { |p| p.team_id == id } + sign_up.each(&:destroy) + end + super + end + + # Delete Review response map + def destroy + review_response_maps.each(&:destroy) + super + end + + # Get the first member of the team + def self.first_member(team_id) + find_by(id: team_id).try(:participants).try(:first) + end + + # Return the files residing in the directory of team submissions + # Main calling method to return the files residing in the directory of team submissions + def submitted_files(path = self.path) + files = [] + files = files(path) if directory_num + files + end + + # REFACTOR BEGIN:: functionality of import,export, handle_duplicate shifted to team.rb + # Import csv file to form teams directly + def self.import(row, assignment_id, options) + unless Assignment.find_by(id: assignment_id) + raise ImportError, 'The assignment with the id "' + assignment_id.to_s + "\" was not found. Create this assignment?" + end + + @assignment_team = prototype + Team.import(row, assignment_id, options, @assignment_team) + end + + # Export the existing teams in a csv file + def self.export(csv, parent_id, options) + @assignment_team = prototype + Team.export(csv, parent_id, options, @assignment_team) + end + + # REFACTOR END:: functionality of import, export handle_duplicate shifted to team.rb + + # Copy the current Assignment team to the CourseTeam + def copy(course_id) + new_team = CourseTeam.create_team_and_node(course_id) + new_team.name = name + new_team.save + copy_members(new_team) + end + + # Add Participants to the current Assignment Team + def add_participant(assignment_id, user) + return if AssignmentParticipant.find_by(parent_id: assignment_id, user_id: user.id) + + AssignmentParticipant.create(parent_id: assignment_id, user_id: user.id, permission_granted: user.master_permission_granted) + end + + def hyperlinks + submitted_hyperlinks.blank? ? [] : YAML.safe_load(submitted_hyperlinks) + end + + # Appends the hyperlink to a list that is stored in YAML format in the DB + # @exception If is hyperlink was already there + # If it is an invalid URL + + def files(directory) + files_list = Dir[directory + '/*'] + files = [] + + files_list.each do |file| + if File.directory?(file) + dir_files = files(file) + dir_files.each { |f| files << f } + end + files << file + end + files + end + + def submit_hyperlink(hyperlink) + hyperlink.strip! + raise 'The hyperlink cannot be empty!' if hyperlink.empty? + + hyperlink = 'http://' + hyperlink unless hyperlink.start_with?('http://', 'https://') + # If not a valid URL, it will throw an exception + response_code = Net::HTTP.get_response(URI(hyperlink)) + raise "HTTP status code: #{response_code}" if response_code =~ /[45][0-9]{2}/ + + hyperlinks = self.hyperlinks + hyperlinks << hyperlink + self.submitted_hyperlinks = YAML.dump(hyperlinks) + save + end + + # Note: This method is not used yet. It is here in the case it will be needed. + # @exception If the index does not exist in the array + + def remove_hyperlink(hyperlink_to_delete) + hyperlinks = self.hyperlinks + hyperlinks.delete(hyperlink_to_delete) + self.submitted_hyperlinks = YAML.dump(hyperlinks) + save + end + + # return the team given the participant + def self.team(participant) + return nil if participant.nil? + + team = nil + teams_users = TeamsUser.where(user_id: participant.user_id) + return nil unless teams_users + + teams_users.each do |teams_user| + if teams_user.team_id == nil + next + end + team = Team.find(teams_user.team_id) + return team if team.assignment_id == participant.assignment_id + end + nil + end + + # Export the fields + def self.export_fields(options) + fields = [] + fields.push('Team Name') + fields.push('Team members') if options[:team_name] == 'false' + fields.push('Assignment Name') + end + + # Remove a team given the team id + def self.remove_team_by_id(id) + old_team = AssignmentTeam.find(id) + old_team.destroy unless old_team.nil? + end + + # Get the path of the team directory + def path + assignment.path + '/' + directory_num.to_s + end + + # Set the directory num for this team + def set_student_directory_num + return if directory_num && (directory_num >= 0) + + max_num = AssignmentTeam.where(parent_id: parent_id).order('directory_num desc').first.directory_num + dir_num = max_num ? max_num + 1 : 0 + update_attributes(directory_num: dir_num) + end + + def received_any_peer_review? + ResponseMap.where(reviewee_id: id, reviewed_object_id: parent_id).any? + end + + # Returns the most recent submission of the team + def most_recent_submission + assignment = Assignment.find(parent_id) + SubmissionRecord.where(team_id: id, assignment_id: assignment.id).order(updated_at: :desc).first + end + + # E-1973 gets the participant id of the currently logged in user, given their user id + # this method assumes that the team is the reviewer since it would be called on + # AssignmentParticipant otherwise + def get_logged_in_reviewer_id(current_user_id) + participants.each do |participant| + return participant.id if participant.user.id == current_user_id + end + nil + end + + # determines if the team contains a participant who is currently logged in + def current_user_is_reviewer?(current_user_id) + get_logged_in_reviewer_id(current_user_id) != nil + end + + # E2121 Refractor create_new_team + def create_new_team(user_id, signuptopic) + t_user = TeamsUser.create(team_id: id, user_id: user_id) + SignedUpTeam.create(topic_id: signuptopic.id, team_id: id, is_waitlisted: 0) + parent = TeamNode.create(parent_id: signuptopic.assignment_id, node_object_id: id) + TeamUserNode.create(parent_id: parent.id, node_object_id: t_user.id) + end + end + \ No newline at end of file diff --git a/app/models/choice_question.rb b/app/models/choice_question.rb new file mode 100644 index 000000000..3a829500b --- /dev/null +++ b/app/models/choice_question.rb @@ -0,0 +1,2 @@ +class ChoiceQuestion < Question +end diff --git a/app/models/due_date.rb b/app/models/due_date.rb index fbebe3bfb..c5a017cb0 100644 --- a/app/models/due_date.rb +++ b/app/models/due_date.rb @@ -55,4 +55,25 @@ def copy(new_assignment_id) new_due_date.parent_id = new_assignment_id new_due_date.save end + + def self.get_next_due_date(assignment_id, topic_id = nil) + if Assignment.find(assignment_id).staggered_deadline? + next_due_date = TopicDueDate.find_by(['parent_id = ? and due_at >= ?', topic_id, Time.zone.now]) + if next_due_date.nil? + topic_due_date_size = TopicDueDate.where(parent_id: topic_id).size + following_assignment_due_dates = AssignmentDueDate.where(parent_id: assignment_id)[topic_due_date_size..-1] + unless following_assignment_due_dates.nil? + following_assignment_due_dates.each do |assignment_due_date| + if assignment_due_date.due_at >= Time.zone.now + next_due_date = assignment_due_date + break + end + end + end + end + else + next_due_date = AssignmentDueDate.find_by(['parent_id = ? && due_at >= ?', assignment_id, Time.zone.now]) + end + next_due_date + end end diff --git a/app/models/questionnaire.rb b/app/models/questionnaire.rb index 43963ad66..41bd05068 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -1,55 +1,177 @@ -class Questionnaire < ApplicationRecord - belongs_to :instructor - has_many :items, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire - before_destroy :check_for_question_associations - has_many :questions - - validate :validate_questionnaire - validates :name, presence: true - validates :max_question_score, :min_question_score, numericality: true - - # clones the contents of a questionnaire, including the questions and associated advice - def self.copy_questionnaire_details(params) - orig_questionnaire = Questionnaire.find(params[:id]) - questions = Item.where(questionnaire_id: params[:id]) - questionnaire = orig_questionnaire.dup - questionnaire.name = 'Copy of ' + orig_questionnaire.name - questionnaire.created_at = Time.zone.now - questionnaire.updated_at = Time.zone.now - questionnaire.save! - questions.each do |item| - new_question = item.dup - new_question.questionnaire_id = questionnaire.id - new_question.save! - end - questionnaire - end + class Questionnaire < ApplicationRecord + belongs_to :instructor + has_many :items, class_name: "Item", foreign_key: "questionnaire_id", dependent: :destroy # the collection of questions associated with this Questionnaire + before_destroy :check_for_question_associations + has_many :questions - # validate the entries for this questionnaire - def validate_questionnaire - errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score < 1 - errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score < 0 - errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score >= max_question_score - results = Questionnaire.where('id <> ? and name = ? and instructor_id = ?', id, name, instructor_id) - errors.add(:name, 'Questionnaire names must be unique.') if results.present? - end + validate :validate_questionnaire + validates :name, presence: true + validates :max_question_score, :min_question_score, numericality: true + + + # after_initialize :post_initialization + # @print_name = 'Review Rubric' + + # class << self + # attr_reader :print_name + # end + + # def post_initialization + # self.display_type = 'Review' + # end + + def symbol + 'review'.to_sym + end + + def get_assessments_for(participant) + participant.reviews + end + + # return the responses for specified round, for varying rubric feature -Yang + def get_assessments_round_for(participant, round) + team = AssignmentTeam.team(participant) + return nil unless team + + team_id = team.id + responses = [] + if participant + maps = ResponseMap.where(reviewee_id: team_id, type: 'ReviewResponseMap') + maps.each do |map| + next if map.response.empty? + + map.response.each do |response| + responses << response if response.round == round && response.is_submitted + end + end + # responses = Response.find(:all, :include => :map, :conditions => ['reviewee_id = ? and type = ?',participant.id, self.to_s]) + responses.sort! { |a, b| a.map.reviewer.fullname <=> b.map.reviewer.fullname } + end + responses + end - # Check_for_question_associations checks if questionnaire has associated questions or not - def check_for_question_associations - if questions.any? - raise ActiveRecord::DeleteRestrictionError.new( "Cannot delete record because dependent questions exist") + # validate the entries for this questionnaire + def validate_questionnaire + errors.add(:max_question_score, 'The maximum item score must be a positive integer.') if max_question_score < 1 + errors.add(:min_question_score, 'The minimum item score must be a positive integer.') if min_question_score < 0 + errors.add(:min_question_score, 'The minimum item score must be less than the maximum.') if min_question_score >= max_question_score + results = Questionnaire.where('id <> ? and name = ? and instructor_id = ?', id, name, instructor_id) + errors.add(:name, 'Questionnaire names must be unique.') if results.present? end - end - def as_json(options = {}) - super(options.merge({ - only: %i[id name private min_question_score max_question_score created_at updated_at questionnaire_type instructor_id], - include: { - instructor: { only: %i[name email fullname password role] - } - } - })).tap do |hash| - hash['instructor'] ||= { id: nil, name: nil } + # Check_for_question_associations checks if questionnaire has associated questions or not + def check_for_question_associations + if questions.any? + raise ActiveRecord::DeleteRestrictionError.new( "Cannot delete record because dependent questions exist") + end + end + + def as_json(options = {}) + super(options.merge({ + only: %i[id name private min_question_score max_question_score created_at updated_at questionnaire_type instructor_id], + include: { + instructor: { only: %i[name email fullname password role] + } + } + })).tap do |hash| + hash['instructor'] ||= { id: nil, name: nil } + end + end + + DEFAULT_MIN_QUESTION_SCORE = 0 # The lowest score that a reviewer can assign to any questionnaire question + DEFAULT_MAX_QUESTION_SCORE = 5 # The highest score that a reviewer can assign to any questionnaire question + DEFAULT_QUESTIONNAIRE_URL = 'http://www.courses.ncsu.edu/csc517'.freeze + QUESTIONNAIRE_TYPES = ['ReviewQuestionnaire', + 'MetareviewQuestionnaire', + 'Author FeedbackQuestionnaire', + 'AuthorFeedbackQuestionnaire', + 'Teammate ReviewQuestionnaire', + 'TeammateReviewQuestionnaire', + 'SurveyQuestionnaire', + 'AssignmentSurveyQuestionnaire', + 'Assignment SurveyQuestionnaire', + 'Global SurveyQuestionnaire', + 'GlobalSurveyQuestionnaire', + 'Course SurveyQuestionnaire', + 'CourseSurveyQuestionnaire', + 'Bookmark RatingQuestionnaire', + 'BookmarkRatingQuestionnaire', + 'QuizQuestionnaire'].freeze + # has_paper_trail + + def get_weighted_score(assignment, scores) + # create symbol for "varying rubrics" feature -Yang + round = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: id).used_in_round + questionnaire_symbol = if round.nil? + symbol + else + (symbol.to_s + round.to_s).to_sym + end + compute_weighted_score(questionnaire_symbol, assignment, scores) + end + + def compute_weighted_score(symbol, assignment, scores) + # aq = assignment_questionnaires.find_by(assignment_id: assignment.id) + aq = AssignmentQuestionnaire.find_by(assignment_id: assignment.id) + + if scores[symbol][:scores][:avg].nil? + 0 + else + scores[symbol][:scores][:avg] * aq.questionnaire_weight / 100.0 end + end + + # Does this questionnaire contain true/false questions? + def true_false_questions? + questions.each { |question| return true if question.type == 'Checkbox' } + false + end + + def delete + assignments.each do |assignment| + raise "The assignment #{assignment.name} uses this questionnaire. + Do you want to delete the assignment?" + end + + questions.each(&:delete) + + node = QuestionnaireNode.find_by(node_object_id: id) + node.destroy if node + + destroy + end + + def max_possible_score + results = Questionnaire.joins('INNER JOIN questions ON questions.questionnaire_id = questionnaires.id') + .select('SUM(questions.weight) * questionnaires.max_question_score as max_score') + .where('questionnaires.id = ?', id) + results[0].max_score + end + + # clones the contents of a questionnaire, including the questions and associated advice + def self.copy_questionnaire_details(params, instructor_id) + orig_questionnaire = Questionnaire.find(params[:id]) + questions = Question.where(questionnaire_id: params[:id]) + questionnaire = orig_questionnaire.dup + questionnaire.instructor_id = instructor_id + questionnaire.name = 'Copy of ' + orig_questionnaire.name + questionnaire.created_at = Time.zone.now + questionnaire.save! + questions.each do |question| + new_question = question.dup + new_question.questionnaire_id = questionnaire.id + new_question.size = '50,3' if (new_question.is_a?(Criterion) || new_question.is_a?(TextResponse)) && new_question.size.nil? + new_question.save! + advices = QuestionAdvice.where(question_id: question.id) + next if advices.empty? + + advices.each do |advice| + new_advice = advice.dup + new_advice.question_id = new_question.id + new_advice.save! + end + end + questionnaire + end + end -end diff --git a/app/models/questionnaire_header.rb b/app/models/questionnaire_header.rb new file mode 100644 index 000000000..32e4d707f --- /dev/null +++ b/app/models/questionnaire_header.rb @@ -0,0 +1,34 @@ +class QuestionnaireHeader < Question + # This method returns what to display if an instructor (etc.) is creating or editing a questionnaire (questionnaires_controller.rb) + def edit(_count) + html = '' + html += 'Remove' + html += '' + html += '' + html += '' + html += '' + html += '' + + html.html_safe + end + + # This method returns what to display if an instructor (etc.) is viewing a questionnaire + def view_question_text + html = ' ' + txt + ' ' + html += '' + type + '' + html += '' + weight.to_s + '' + html += '—' + html += '' + html.html_safe + end + + def complete + txt + end + + def view_completed_question; end + end + \ No newline at end of file diff --git a/app/models/response.rb b/app/models/response.rb index dd6519c80..01384cac2 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -57,7 +57,7 @@ def aggregate_questionnaire_score sum end - def aggregate_assessment_scores(assessments, questions) + def self.aggregate_assessment_scores(assessments, questions) scores = {} if assessments.present? scores[:max] = -999_999_999 @@ -95,7 +95,7 @@ def aggregate_assessment_scores(assessments, questions) # assessment - specifies the assessment for which the total score is being calculated # questions - specifies the list of questions being evaluated in the assessment # Called in: bookmarks_controller.rb (specific_average_score), grades_helper.rb (score_vector), response.rb (self.score), scoring.rb - def assessment_score(params) + def self.assessment_score(params) @response = params[:response].last return -1.0 if @response.nil? @@ -107,7 +107,6 @@ def assessment_score(params) sum_of_weights = 0 @questionnaire = Questionnaire.find(questions.first.questionnaire_id) - # Retrieve data for questionnaire (max score, sum of scores, weighted scores, etc.) questionnaire_data = ScoreView.questionnaire_data(questions[0].questionnaire_id, @response.id) weighted_score = questionnaire_data.weighted_score.to_f unless questionnaire_data.weighted_score.nil? sum_of_weights = questionnaire_data.sum_of_weights.to_f @@ -130,33 +129,12 @@ def assessment_score(params) # Compute total score for this assignment by summing the scores given on all questionnaires. # Only scores passed in are included in this sum. # Called in: scoring.rb - def compute_total_score(assignment, scores) + def self.compute_total_score(assignment, scores) total = 0 assignment.questionnaires.each { |questionnaire| total += questionnaire.get_weighted_score(assignment, scores) } total end - # Computes and returns the scores of assignment for participants and teams - # Returns data in the format of - # { - # :particpant => { - # : => participant_scores(participant, questions), - # : => participant_scores(participant, questions) - # }, - # :teams => { - # :0 => {:team => team, - # :scores => assignment.vary_by_round? ? - # merge_grades_by_rounds(assignment, grades_by_rounds, total_num_of_assessments, total_score) - # : aggregate_assessment_scores(assessments, questions[:review]) - # } , - # :1 => {:team => team, - # :scores => assignment.vary_by_round? ? - # merge_grades_by_rounds(assignment, grades_by_rounds, total_num_of_assessments, total_score) - # : aggregate_assessment_scores(assessments, questions[:review]) - # } , - # } - # } - # Called in: grades_controller.rb (view), assignment.rb (self.export) def self.review_grades(assignment, questions) scores = { participants: {}, teams: {} } assignment.participants.each do |participant| @@ -166,14 +144,13 @@ def self.review_grades(assignment, questions) scores[:teams][index.to_s.to_sym] = { team: team, scores: {} } if assignment.varying_rubrics_by_round? grades_by_rounds, total_num_of_assessments, total_score = compute_grades_by_rounds(assignment, questions, team) - # merge the grades from multiple rounds scores[:teams][index.to_s.to_sym][:scores] = merge_grades_by_rounds(assignment, grades_by_rounds, total_num_of_assessments, total_score) else assessments = ReviewResponseMap.assessments_for(team) scores[:teams][index.to_s.to_sym][:scores] = aggregate_assessment_scores(assessments, questions[:review]) end end - scores + return scores end @@ -214,7 +191,7 @@ def self.participant_scores(participant, questions) # returns all the associated reviews with a participant, indexed under :assessments # returns the score assigned for the TOTAL body of responses associated with the user # Called in: scoring.rb - def compute_assignment_score(participant, questions, scores) + def self.compute_assignment_score(participant, questions, scores) participant.assignment.questionnaires.each do |questionnaire| round = AssignmentQuestionnaire.find_by(assignment_id: participant.assignment.id, questionnaire_id: questionnaire.id).used_in_round # create symbol for "varying rubrics" feature -Yang @@ -239,7 +216,7 @@ def compute_assignment_score(participant, questions, scores) # this will be called when the assignment has various rounds, so we need to aggregate the scores across rounds # achieves this by returning all the reviews, no longer delineated by round, and by returning the max, min and average # Called in: scoring.rb - def merge_scores(participant, scores) + def self.merge_scores(participant, scores) review_sym = 'review'.to_sym scores[review_sym] = {} scores[review_sym][:assessments] = [] @@ -270,7 +247,7 @@ def merge_scores(participant, scores) end # Called in: scoring.rb - def update_max_or_min(scores, round_sym, review_sym, symbol) + def self.update_max_or_min(scores, round_sym, review_sym, symbol) op = :< if symbol == :max op = :> if symbol == :min # check if there is a max/min score for this particular round @@ -324,8 +301,11 @@ def compute_avg_and_ranges_hash(assignment) end def self.extract_team_averages(scores) - scores[:teams].reject! { |_k, v| v[:scores][:avg].nil? } - scores[:teams].map { |_k, v| v[:scores][:avg].to_i } + # puts scores + # scores[:teams].reject! { |_k, v| v[:scores][:avg].nil? } + # scores[:teams].map { |_k, v| v[:scores][:avg].to_i } + scores.reject! { |_k, v| v[:scores][:avg].nil? } + scores.map { |_k, v| v[:scores][:avg].to_i } end def self.average_team_scores(array) @@ -394,7 +374,7 @@ def scores_non_varying_rubrics(assignment, review_scores, response_maps) # Below private methods are extracted and added as part of refactoring project E2009 - Spring 2020 # This method computes and returns grades by rounds, total_num_of_assessments and total_score # when the assignment has varying rubrics by round -def compute_grades_by_rounds(assignment, questions, team) +def self.compute_grades_by_rounds(assignment, questions, team) grades_by_rounds = {} total_score = 0 total_num_of_assessments = 0 # calculate grades for each rounds @@ -409,7 +389,7 @@ def compute_grades_by_rounds(assignment, questions, team) end # merge the grades from multiple rounds -def merge_grades_by_rounds(assignment, grades_by_rounds, num_of_assessments, total_score) +def self.merge_grades_by_rounds(assignment, grades_by_rounds, num_of_assessments, total_score) team_scores = { max: 0, min: 0, avg: nil } return team_scores if num_of_assessments.zero? diff --git a/app/models/review_questionnaire.rb b/app/models/review_questionnaire.rb new file mode 100644 index 000000000..2f056d6d2 --- /dev/null +++ b/app/models/review_questionnaire.rb @@ -0,0 +1,43 @@ +class ReviewQuestionnaire < Questionnaire + after_initialize :post_initialization + @print_name = 'Review Rubric' + + class << self + attr_reader :print_name + end + + def post_initialization + self.display_type = 'Review' + end + + def symbol + 'review'.to_sym + end + + def get_assessments_for(participant) + participant.reviews + end + + # return the responses for specified round, for varying rubric feature -Yang + def get_assessments_round_for(participant, round) + team = AssignmentTeam.team(participant) + return nil unless team + + team_id = team.id + responses = [] + if participant + maps = ResponseMap.where(reviewee_id: team_id, type: 'ReviewResponseMap') + maps.each do |map| + next if map.response.empty? + + map.response.each do |response| + responses << response if response.round == round && response.is_submitted + end + end + # responses = Response.find(:all, :include => :map, :conditions => ['reviewee_id = ? and type = ?',participant.id, self.to_s]) + responses.sort! { |a, b| a.map.reviewer.fullname <=> b.map.reviewer.fullname } + end + responses + end + end + \ No newline at end of file diff --git a/app/models/review_response_map.rb b/app/models/review_response_map.rb index c4f852657..bb16c51ba 100644 --- a/app/models/review_response_map.rb +++ b/app/models/review_response_map.rb @@ -7,4 +7,18 @@ class ReviewResponseMap < ResponseMap def response_assignment return assignment end + + def self.get_responses_for_team_round(team, round) + responses = [] + if team.id + maps = ResponseMap.where(reviewee_id: team.id, type: 'ReviewResponseMap') + maps.each do |map| + if map.response.any? && map.response.reject { |r| (r.round != round || !r.is_submitted) }.any? + responses << map.response.reject { |r| (r.round != round || !r.is_submitted) }.last + end + end + responses.sort! { |a, b| a.map.reviewer.fullname <=> b.map.reviewer.fullname } + end + responses + end end diff --git a/app/models/score_view.rb b/app/models/score_view.rb new file mode 100644 index 000000000..fc3d9344e --- /dev/null +++ b/app/models/score_view.rb @@ -0,0 +1,13 @@ +class ScoreView < ApplicationRecord + # setting this to false so that factories can be created + # to test the grading of weighted quiz questionnaires + def readonly? + false + end + + def self.questionnaire_data(questionnaire_id, response_id) + questionnaire_data = ScoreView.find_by_sql ["SELECT q1_max_question_score ,SUM(question_weight) as sum_of_weights,SUM(question_weight * s_score) as weighted_score FROM score_views WHERE type in('Criterion', 'Scale') AND q1_id = ? AND s_response_id = ? GROUP BY q1_max_question_score", questionnaire_id, response_id] + questionnaire_data[0] + end + end + \ No newline at end of file diff --git a/app/models/scored_question.rb b/app/models/scored_question.rb new file mode 100644 index 000000000..5853bfe34 --- /dev/null +++ b/app/models/scored_question.rb @@ -0,0 +1,18 @@ +class ScoredQuestion < ChoiceQuestion + validates :weight, presence: true # user must specify a weight for a question + validates :weight, numericality: true # the weight must be numeric + + def edit; end + + def view_question_text; end + + def complete; end + + def view_completed_question; end + + def self.compute_question_score(response_id) + answer = Answer.find_by(question_id: id, response_id: response_id) + weight * answer.answer + end + end + \ No newline at end of file diff --git a/app/models/signed_up_team.rb b/app/models/signed_up_team.rb index 73c5fd43f..d4694f688 100644 --- a/app/models/signed_up_team.rb +++ b/app/models/signed_up_team.rb @@ -2,7 +2,7 @@ class SignedUpTeam < ApplicationRecord belongs_to :sign_up_topic belongs_to :team - def self.topic_id(assignment_id, user_id) + def self.find_topic_id_for_user(assignment_id, user_id) # team_id variable represents the team_id for this user in this assignment team_id = TeamsUser.team_id(assignment_id, user_id) topic_id_by_team_id(team_id) if team_id @@ -13,7 +13,7 @@ def self.topic_id_by_team_id(team_id) if signed_up_teams.blank? nil else - signed_up_teams.first.topic_id + signed_up_teams.first.sign_up_topic_id end end end diff --git a/app/models/tag_prompt.rb b/app/models/tag_prompt.rb new file mode 100644 index 000000000..e2484626a --- /dev/null +++ b/app/models/tag_prompt.rb @@ -0,0 +1,81 @@ +class TagPrompt < ApplicationRecord + validates :prompt, presence: true + validates :desc, presence: true + validates :control_type, presence: true + + def html_control(tag_prompt_deployment, answer, user_id) + html = '' + unless answer.nil? + stored_tags = AnswerTag.where(tag_prompt_deployment_id: tag_prompt_deployment.id, answer_id: answer.id, user_id: user_id) + + length_valid = false + if tag_prompt_deployment.answer_length_threshold.nil? + length_valid = true + else + length_valid = true unless answer.comments.nil? || answer.comments.length <= tag_prompt_deployment.answer_length_threshold + end + + if length_valid && answer.question.type.eql?(tag_prompt_deployment.question_type) + case control_type.downcase + when 'slider' + html = slider_control(answer, tag_prompt_deployment, stored_tags) + when 'checkbox' + html = checkbox_control(answer, tag_prompt_deployment, stored_tags) + end + end + end + html.html_safe + end + + def checkbox_control(answer, tag_prompt_deployment, stored_tags) + html = '' + value = '0' + + if stored_tags.count > 0 + tag = stored_tags.first + value = tag.value.to_s + end + + element_id = answer.id.to_s + '_' + id.to_s + control_id = 'tag_prompt_' + element_id + + html += '
' + html += '' + html += '' + html += '
' + html + end + + def slider_control(answer, tag_prompt_deployment, stored_tags) + html = '' + value = '0' + if stored_tags.count > 0 + tag = stored_tags.first + value = tag.value.to_s + end + element_id = answer.id.to_s + '_' + id.to_s + control_id = 'tag_prompt_' + element_id + no_text_class = 'toggle-false-msg' + yes_text_class = 'toggle-true-msg' + + # change the color of the label based on its value + if value.to_i < 0 + no_text_class += ' textActive' + elsif value.to_i > 0 + yes_text_class += ' textActive' + end + + html += '
' + html += '
No
' + html += '
' + html += ' ' + html += '
' + html += '
Yes
' + html += '
' + prompt.to_s + '
' + html += '
' + + html + end + end + \ No newline at end of file diff --git a/app/models/tag_prompt_deployment.rb b/app/models/tag_prompt_deployment.rb new file mode 100644 index 000000000..f4cd0d57b --- /dev/null +++ b/app/models/tag_prompt_deployment.rb @@ -0,0 +1,80 @@ +class TagPromptDeployment < ApplicationRecord + belongs_to :tag_prompt + belongs_to :assignment + belongs_to :questionnaire + has_many :answer_tags, dependent: :destroy + + require 'time' + + def tag_prompt + TagPrompt.find(tag_prompt_id) + end + + def get_number_of_taggable_answers(user_id) + team = Team.joins(:teams_users).where(team_users: { parent_id: assignment_id }, user_id: user_id) + responses = Response.joins(:response_maps).where(response_maps: { reviewed_object_id: assignment.id, reviewee_id: team.id }) + questions = Question.where(questionnaire_id: questionnaire.id, type: question_type) + + unless responses.empty? || questions.empty? + responses_ids = responses.map(&:id) + questions_ids = questions.map(&:id) + + answers = Answer.where(question_id: questions_ids, response_id: responses_ids) + + answers = answers.where(conditions: "length(comments) < #{answer_length_threshold}") unless answer_length_threshold.nil? + return answers.count + end + 0 + end + + def assignment_tagging_progress + teams = Team.where(parent_id: assignment_id) + questions = Question.where(questionnaire_id: questionnaire.id, type: question_type) + questions_ids = questions.map(&:id) + user_answer_tagging = [] + unless teams.empty? || questions.empty? + teams.each do |team| + if assignment.varying_rubrics_by_round? + responses = [] + 1.upto(assignment.rounds_of_reviews).each do |round| + responses += ReviewResponseMap.get_responses_for_team_round(team, round) + end + else + responses = ResponseMap.assessments_for(team) + end + responses_ids = responses.map(&:id) + answers = Answer.where(question_id: questions_ids, response_id: responses_ids) + + answers = answers.select { |answer| answer.comments.length > answer_length_threshold } unless answer_length_threshold.nil? + answers_ids = answers.map(&:id) + teams_users = TeamsUser.where(team_id: team.id) + users = teams_users.map { |teams_user| User.find(teams_user.user_id) } + + users.each do |user| + tags = AnswerTag.where(tag_prompt_deployment_id: id, user_id: user.id, answer_id: answers_ids) + tagged_answers_ids = tags.map(&:answer_id) + + # E2082 Track_Time_Between_Successive_Tag_Assignments + # Extract time where each tag is generated / modified + tag_updated_times = tags.map(&:updated_at) + # tag_updated_times.sort_by{|time_string| Time.parse(time_string)}.reverse + tag_updated_times.sort.reverse + number_of_updated_time = tag_updated_times.length + tag_update_intervals = [] + 1.upto(number_of_updated_time - 1).each do |i| + tag_update_intervals.append(tag_updated_times[i] - tag_updated_times[i - 1]) + end + + percentage = answers.count.zero? ? '-' : format('%.1f', tags.count.to_f / answers.count * 100) + not_tagged_answers = answers.reject { |answer| tagged_answers_ids.include?(answer.id) } + + # E2082 Adding tag_update_intervals as information that should be passed + answer_tagging = VmUserAnswerTagging.new(user, percentage, tags.count, not_tagged_answers.count, answers.count, tag_update_intervals) + user_answer_tagging.append(answer_tagging) + end + end + end + user_answer_tagging + end + end + \ No newline at end of file diff --git a/app/models/teams_user.rb b/app/models/teams_user.rb index fa6c9eb93..41695a99b 100644 --- a/app/models/teams_user.rb +++ b/app/models/teams_user.rb @@ -29,7 +29,7 @@ def self.team_id(assignment_id, user_id) next end team = Team.find(teams_user.team_id) - if team.parent_id == assignment_id + if team.assignment_id == assignment_id team_id = teams_user.team_id break end diff --git a/app/models/vm_question_response.rb b/app/models/vm_question_response.rb new file mode 100644 index 000000000..e38137502 --- /dev/null +++ b/app/models/vm_question_response.rb @@ -0,0 +1,197 @@ +# This is a new model create by E1577 (heat map) +# represents each table in the view_team view. +# the important piece to note is that the @listofrows is a list of type VmQuestionResponse_Row, which represents a row of the heatgrid table. +class VmQuestionResponse + attr_reader :name, :rounds, :round, :questionnaire_type, :questionnaire_display_type, :list_of_reviews, :list_of_rows, :list_of_reviewers, :max_score + + @questionnaire = nil + @assignment = nil + + def initialize(questionnaire, assignment = nil, round = nil) + @assignment = assignment + @questionnaire = questionnaire + @round = round + if questionnaire.questionnaire_type == 'ReviewQuestionnaire' + @round = round || AssignmentQuestionnaire.find_by(assignment_id: @assignment.id, questionnaire_id: questionnaire.id).used_in_round + end + + @rounds = @assignment.rounds_of_reviews + + @list_of_rows = [] + @list_of_reviewers = [] + @list_of_reviews = [] + @list_of_team_participants = [] + @max_score = questionnaire.max_question_score + @questionnaire_type = questionnaire.questionnaire_type + @questionnaire_display_type = questionnaire.display_type + @rounds = rounds + + @name = questionnaire.name + end + + def add_questions(questions) + questions.each do |question| + # Get the maximum score for this question. For some unknown, godforsaken reason, the max + # score for the question is stored not on the question, but on the questionnaire. Neat. + corresponding_questionnaire = question.questionnaire + question_max_score = corresponding_questionnaire.max_question_score + # if this question is a header (table header, section header, column header), ignore this question + unless question.is_a? QuestionnaireHeader + row = VmQuestionResponseRow.new(question.txt, question.id, question.weight, question_max_score, question.seq) + @list_of_rows << row + end + end + end + + def add_reviews(participant, team, vary) + if @questionnaire_type == 'ReviewQuestionnaire' + reviews = if vary + ReviewResponseMap.get_responses_for_team_round(team, @round) + else + ReviewResponseMap.assessments_for(team) + end + reviews.each do |review| + review_mapping = ReviewResponseMap.find(review.map_id) + if review_mapping.present? + participant = Participant.find_by(user_id: review_mapping.reviewer_id) + @list_of_reviewers << participant + end + end + @list_of_reviews = reviews + elsif @questionnaire_type == 'AuthorFeedbackQuestionnaire' # ISSUE E-1967 updated + reviews = [] + # finding feedbacks where current participant of assignment (author) is reviewer + feedbacks = FeedbackResponseMap.where(reviewer_id: participant.id) + feedbacks.each do |feedback| + # finding the participant ids for each reviewee of feedback + # participant is really reviewee here. + participant = Participant.find_by(id: feedback.reviewee_id) + # finding the all the responses for the feedback + response = Response.where(map_id: feedback.id).order('updated_at').last + if response + reviews << response + @list_of_reviews << response + end + @list_of_reviewers << participant + end + elsif @questionnaire_type == 'TeammateReviewQuestionnaire' + reviews = participant.teammate_reviews + reviews.each do |review| + review_mapping = TeammateReviewResponseMap.find_by(id: review.map_id) + participant = Participant.find(review_mapping.reviewer_id) + # commenting out teamreviews. I just realized that teammate reviews are hidden during the current semester, + # and I don't know how to implement the logic, so I'm being safe. + @list_of_reviewers << participant + @list_of_reviews << review + end + elsif @questionnaire_type == 'MetareviewQuestionnaire' + reviews = participant.metareviews + reviews.each do |review| + review_mapping = MetareviewResponseMap.find_by(id: review.map_id) + participant = Participant.find(review_mapping.reviewer_id) + @list_of_reviewers << participant + @list_of_reviews << review + end + end + reviews.each do |review| + answers = Answer.where(response_id: review.id) + answers.each do |answer| + add_answer(answer) + end + end + end + + def display_team_members(ip_address = nil) + @output = '' + if @questionnaire_type == 'MetareviewQuestionnaire' || @questionnaire_type == 'ReviewQuestionnaire' + @output = 'Team members:' + @list_of_team_participants.each do |participant| + @output = @output + ' (' + participant.fullname(ip_address) + ') ' + end + + end + + @output + end + + def add_team_members(team) + @list_of_team_participants = team.participants + end + + def max_score_for_questionnaire + @max_score * @list_of_rows.length + end + + def add_answer(answer) + # We want to add each response score from this review (answer) to its corresponding + # question row. + @list_of_rows.each do |row| + next unless row.question_id == answer.question_id + + # Go ahead and calculate what the color code for this score should be. + question_max_score = row.question_max_score + + # This calculation is a little tricky. We're going to find the percentage for this score, + # multiply it by 5, and then take the ceiling of that value to get the color code. This + # should work for any point value except 0 (which we'll handle separately). + color_code_number = 0 + if answer.answer.is_a? Numeric + color_code_number = ((answer.answer.to_f / question_max_score.to_f) * 5.0) + # Color code c0 is reserved for null spaces in the table which will be gray. + color_code_number = 1 if color_code_number.zero? + end + + # Find out the tag prompts associated with the question + tag_deps = TagPromptDeployment.where(questionnaire_id: @questionnaire.id, assignment_id: @assignment.id) + vm_tag_prompts = [] + + question = Question.find(answer.question_id) + + # check if the tag prompt applies for this question type and if the comment length is above the threshold + # if it does, then associate this answer with the tag_prompt and tag deployment (the setting) + tag_deps.each do |tag_dep| + if (tag_dep.question_type == question.question_type) && (answer.comments.length > tag_dep.answer_length_threshold) + vm_tag_prompts.append(VmTagPromptAnswer.new(answer, TagPrompt.find(tag_dep.tag_prompt_id), tag_dep)) + end + end + # end tag_prompt code + + # Now construct the color code and we're good to go! + color_code = "c#{color_code_number}" + row.score_row.push(VmQuestionResponseScoreCell.new(answer.answer, color_code, answer.comments, vm_tag_prompts)) + end + end + + # This method calls all the methods that are responsible for calculating different metrics.If any new metric is introduced, please call the method that calculates the metric values from this method. + def calculate_metrics + number_of_comments_greater_than_10_words + number_of_comments_greater_than_20_words + end + + # This method is responsible for checking whether a review comment contains more than 10 words. + def number_of_comments_greater_than_10_words + @list_of_reviews.each do |review| + answers = Answer.where(response_id: review.id) + answers.each do |answer| + @list_of_rows.each do |row| + row.metric_hash["> 10 Word Comments"] = 0 if row.metric_hash["> 10 Word Comments"].nil? + row.metric_hash["> 10 Word Comments"] = row.metric_hash["> 10 Word Comments"] + 1 if row.question_id == answer.question_id && answer.comments && answer.comments.split.size > 10 + end + end + end + end + + # In case if new metirc is added. This is a dummy metric added for manual testing and will be removed. + def number_of_comments_greater_than_20_words + @list_of_reviews.each do |review| + answers = Answer.where(response_id: review.id) + answers.each do |answer| + @list_of_rows.each do |row| + row.metric_hash["> 20 Word Comments"] = 0 if row.metric_hash["> 20 Word Comments"].nil? + row.metric_hash["> 20 Word Comments"] = row.metric_hash["> 20 Word Comments"] + 1 if row.question_id == answer.question_id && answer.comments && answer.comments.split.size > 20 + end + end + end + end + end + \ No newline at end of file diff --git a/app/models/vm_question_response_row.rb b/app/models/vm_question_response_row.rb new file mode 100644 index 000000000..64b2a7460 --- /dev/null +++ b/app/models/vm_question_response_row.rb @@ -0,0 +1,44 @@ +# represents each row of a heatgrid-table, which is represented by the vm_question_response class. +class VmQuestionResponseRow + attr_reader :question_seq, :question_text, :question_id, :score_row, :weight + attr_accessor :metric_hash + + def initialize(question_text, question_id, weight, question_max_score, seq) + @question_text = question_text + @weight = weight + @question_id = question_id + @question_seq = seq + @question_max_score = question_max_score + @score_row = [] + @metric_hash = {} + end + + # the question max score is the max score of the questionnaire, except if the question is a true/false, in which case + # the max score is one. + def question_max_score + question = Question.find(question_id) + if question.question_type == 'Checkbox' + 1 + elsif question.is_a? ScoredQuestion + @question_max_score + else + 'N/A' + end + end + + def average_score_for_row + row_average_score = 0.0 + no_of_columns = 0.0 # Counting reviews that are not null + @score_row.each do |score| + if score.score_value.is_a? Numeric + no_of_columns += 1 + row_average_score += score.score_value.to_f + end + end + unless no_of_columns.zero? + row_average_score /= no_of_columns + row_average_score.round(2) + end + end + end + \ No newline at end of file diff --git a/app/models/vm_question_response_score_cell.rb b/app/models/vm_question_response_score_cell.rb new file mode 100644 index 000000000..b279247b1 --- /dev/null +++ b/app/models/vm_question_response_score_cell.rb @@ -0,0 +1,18 @@ +# represents each score cell of the heatgrid table. +class VmQuestionResponseScoreCell + def initialize(score_value, color_code, comments, vmprompts = nil) + @score_value = score_value + @color_code = color_code + @comment = comments + @vm_prompts = vmprompts + end + + attr_reader :score_value + + attr_reader :comment + + attr_reader :color_code + + attr_reader :vm_prompts + end + \ No newline at end of file diff --git a/config/initializers/load_config.rb b/config/initializers/load_config.rb new file mode 100644 index 000000000..ac1b10c30 --- /dev/null +++ b/config/initializers/load_config.rb @@ -0,0 +1 @@ +WEBSERVICE_CONFIG = YAML.load_file("#{Rails.root}/config/webservices.yml")[Rails.env] \ No newline at end of file diff --git a/config/webservices.yml b/config/webservices.yml new file mode 100644 index 000000000..bc6bafce8 --- /dev/null +++ b/config/webservices.yml @@ -0,0 +1,8 @@ +development: + metareview_webservice_url: 'http://peerlogic.csc.ncsu.edu/metareview/metareviewgenerator/' + reputation_web_service_url: 'http://peerlogic.csc.ncsu.edu/reputation/calculations/reputation_algorithms' + summary_webservice_url: 'http://peerlogic.csc.ncsu.edu/sum/v1.0/summary/8/lsa' + topic_bidding_webservice_url: 'http://peerlogic.csc.ncsu.edu/intelligent_assignment/merge_teams' + heatmap_webservice_url: 'http://peerlogic.csc.ncsu.edu/reviewsentiment/configure' + sentiment_webservice_url: 'http://peerlogic.csc.ncsu.edu/sentiment/' + review_bidding_webservice_url: 'https://app-csc517.herokuapp.com/match_topics' diff --git a/db/migrate/20250411154148_add_name_to_teams.rb b/db/migrate/20250411154148_add_name_to_teams.rb new file mode 100644 index 000000000..d18f94bcd --- /dev/null +++ b/db/migrate/20250411154148_add_name_to_teams.rb @@ -0,0 +1,5 @@ +class AddNameToTeams < ActiveRecord::Migration[8.0] + def change + add_column :teams, :name, :string + end +end diff --git a/db/migrate/20250413164318_add_type_to_response_maps.rb b/db/migrate/20250413164318_add_type_to_response_maps.rb new file mode 100644 index 000000000..991706ccb --- /dev/null +++ b/db/migrate/20250413164318_add_type_to_response_maps.rb @@ -0,0 +1,5 @@ +class AddTypeToResponseMaps < ActiveRecord::Migration[8.0] + def change + add_column :response_maps, :type, :string + end +end diff --git a/db/migrate/20250413170704_add_round_and_version_num_to_responses.rb b/db/migrate/20250413170704_add_round_and_version_num_to_responses.rb new file mode 100644 index 000000000..2b096eb08 --- /dev/null +++ b/db/migrate/20250413170704_add_round_and_version_num_to_responses.rb @@ -0,0 +1,6 @@ +class AddRoundAndVersionNumToResponses < ActiveRecord::Migration[8.0] + def change + add_column :responses, :round, :integer + add_column :responses, :version_num, :integer + end +end diff --git a/db/migrate/20250414180932_add_type_to_questions.rb b/db/migrate/20250414180932_add_type_to_questions.rb new file mode 100644 index 000000000..410ba61a3 --- /dev/null +++ b/db/migrate/20250414180932_add_type_to_questions.rb @@ -0,0 +1,5 @@ +class AddTypeToQuestions < ActiveRecord::Migration[8.0] + def change + add_column :questions, :type, :string + end +end diff --git a/db/migrate/20250414182039_create_score_views.rb b/db/migrate/20250414182039_create_score_views.rb new file mode 100644 index 000000000..e3c7acee4 --- /dev/null +++ b/db/migrate/20250414182039_create_score_views.rb @@ -0,0 +1,27 @@ +class CreateScoreViews < ActiveRecord::Migration[8.0] + def change + create_table :score_views do |t| + t.integer :question_weight # No need to specify limit: 10 + t.string :type, limit: 255 + t.integer :q1_id + t.string :q1_name, limit: 255 + t.integer :q1_instructor_id + t.boolean :q1_private, default: false + t.integer :q1_min_question_score + t.integer :q1_max_question_score + t.datetime :q1_created_at + t.datetime :q1_updated_at + t.string :q1_type, limit: 255 + t.string :q1_display_type, limit: 255 + t.integer :ques_id + t.integer :ques_questionnaire_id + t.integer :s_id + t.integer :s_question_id + t.integer :s_score + t.text :s_comments + t.integer :s_response_id + + t.timestamps + end + end +end diff --git a/db/migrate/20250415010637_remove_type_from_questions.rb b/db/migrate/20250415010637_remove_type_from_questions.rb new file mode 100644 index 000000000..b9a490707 --- /dev/null +++ b/db/migrate/20250415010637_remove_type_from_questions.rb @@ -0,0 +1,5 @@ +class RemoveTypeFromQuestions < ActiveRecord::Migration[8.0] + def change + remove_column :questions, :type, :string + end +end diff --git a/db/migrate/20250415025716_add_questionnaire_weight_to_assignment_questionnaires.rb b/db/migrate/20250415025716_add_questionnaire_weight_to_assignment_questionnaires.rb new file mode 100644 index 000000000..93e25b309 --- /dev/null +++ b/db/migrate/20250415025716_add_questionnaire_weight_to_assignment_questionnaires.rb @@ -0,0 +1,5 @@ +class AddQuestionnaireWeightToAssignmentQuestionnaires < ActiveRecord::Migration[8.0] + def change + add_column :assignment_questionnaires, :questionnaire_weight, :integer + end +end diff --git a/db/migrate/20250418051410_create_tag_prompt_assignments.rb b/db/migrate/20250418051410_create_tag_prompt_assignments.rb new file mode 100644 index 000000000..a93e940a4 --- /dev/null +++ b/db/migrate/20250418051410_create_tag_prompt_assignments.rb @@ -0,0 +1,17 @@ +class CreateTagPromptAssignments < ActiveRecord::Migration[8.0] + def change + create_table :tag_prompt_assignments do |t| + t.integer :tag_prompt_id, null: false + t.integer :assignment_id, null: false + t.integer :questionnaire_id, null: false + t.string :question_type, limit: 255 + t.integer :answer_length_threshold + + t.timestamps + end + + add_index :tag_prompt_assignments, :tag_prompt_id + add_index :tag_prompt_assignments, :assignment_id + add_index :tag_prompt_assignments, :questionnaire_id + end +end diff --git a/db/migrate/20250418051607_rename_tag_prompt_assignments_to_tag_prompt_deployments.rb b/db/migrate/20250418051607_rename_tag_prompt_assignments_to_tag_prompt_deployments.rb new file mode 100644 index 000000000..33effcf7a --- /dev/null +++ b/db/migrate/20250418051607_rename_tag_prompt_assignments_to_tag_prompt_deployments.rb @@ -0,0 +1,5 @@ +class RenameTagPromptAssignmentsToTagPromptDeployments < ActiveRecord::Migration[8.0] + def change + rename_table :tag_prompt_assignments, :tag_prompt_deployments + end +end diff --git a/db/migrate/20250418051819_create_task_prompts.rb b/db/migrate/20250418051819_create_task_prompts.rb new file mode 100644 index 000000000..56414af0c --- /dev/null +++ b/db/migrate/20250418051819_create_task_prompts.rb @@ -0,0 +1,11 @@ +class CreateTaskPrompts < ActiveRecord::Migration[8.0] + def change + create_table :task_prompts do |t| + t.string :prompt, limit: 255 + t.string :desc, limit: 255 + t.string :control_type, limit: 255 + + t.timestamps + end + end +end diff --git a/db/migrate/20250418052239_rename_task_prompts_to_tag_prompts.rb b/db/migrate/20250418052239_rename_task_prompts_to_tag_prompts.rb new file mode 100644 index 000000000..cedd4eb68 --- /dev/null +++ b/db/migrate/20250418052239_rename_task_prompts_to_tag_prompts.rb @@ -0,0 +1,5 @@ +class RenameTaskPromptsToTagPrompts < ActiveRecord::Migration[8.0] + def change + rename_table :task_prompts, :tag_prompts + end +end diff --git a/db/schema.rb b/db/schema.rb index c1c1ec801..9d3e25267 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_03_23_040421) do +ActiveRecord::Schema[8.0].define(version: 2025_04_18_052239) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -43,6 +43,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "used_in_round" + t.integer "questionnaire_weight" t.index ["assignment_id"], name: "fk_aq_assignments_id" t.index ["questionnaire_id"], name: "fk_aq_questionnaire_id" end @@ -304,6 +305,7 @@ t.integer "reviewee_id", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "type" t.index ["reviewer_id"], name: "fk_response_map_reviewer" end @@ -313,6 +315,8 @@ t.boolean "is_submitted", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "round" + t.integer "version_num" t.index ["map_id"], name: "fk_response_response_map" end @@ -325,6 +329,30 @@ t.index ["parent_id"], name: "fk_rails_4404228d2f" end + create_table "score_views", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.integer "question_weight" + t.string "type" + t.integer "q1_id" + t.string "q1_name" + t.integer "q1_instructor_id" + t.boolean "q1_private", default: false + t.integer "q1_min_question_score" + t.integer "q1_max_question_score" + t.datetime "q1_created_at" + t.datetime "q1_updated_at" + t.string "q1_type" + t.string "q1_display_type" + t.integer "ques_id" + t.integer "ques_questionnaire_id" + t.integer "s_id" + t.integer "s_question_id" + t.integer "s_score" + t.text "s_comments" + t.integer "s_response_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "sign_up_topics", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.text "topic_name", null: false t.bigint "assignment_id", null: false @@ -362,10 +390,32 @@ t.index ["user_id"], name: "index_ta_mappings_on_user_id" end + create_table "tag_prompt_deployments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.integer "tag_prompt_id", null: false + t.integer "assignment_id", null: false + t.integer "questionnaire_id", null: false + t.string "question_type" + t.integer "answer_length_threshold" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["assignment_id"], name: "index_tag_prompt_deployments_on_assignment_id" + t.index ["questionnaire_id"], name: "index_tag_prompt_deployments_on_questionnaire_id" + t.index ["tag_prompt_id"], name: "index_tag_prompt_deployments_on_tag_prompt_id" + end + + create_table "tag_prompts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "prompt" + t.string "desc" + t.string "control_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "teams", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "assignment_id", null: false + t.string "name" t.index ["assignment_id"], name: "index_teams_on_assignment_id" end From aa1f1652f1cc39856fbe066952161bc36f91dfe8 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sun, 20 Apr 2025 12:39:37 -0400 Subject: [PATCH 35/41] participants and reviews --- app/models/participant.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/participant.rb b/app/models/participant.rb index 212981686..d91a0da51 100644 --- a/app/models/participant.rb +++ b/app/models/participant.rb @@ -4,6 +4,7 @@ class Participant < ApplicationRecord belongs_to :assignment, foreign_key: 'assignment_id', inverse_of: false has_many :join_team_requests, dependent: :destroy belongs_to :team, optional: true + has_many :reviews, class_name: 'ResponseMap', foreign_key: 'reviewer_id', dependent: :destroy, inverse_of: false delegate :course, to: :assignment From 8515d16d0f28ac0b46c3c577ae065176591f0282 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Sun, 20 Apr 2025 18:17:41 -0400 Subject: [PATCH 36/41] Updated unit tests for controller --- app/controllers/api/v1/grades_controller.rb | 4 -- .../requests/api/v1/grades_controller_spec.rb | 72 +++++++++++-------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 7c3126838..3cba68101 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -25,8 +25,6 @@ def view_my_scores participant = AssignmentParticipant.find(params[:id].to_i) assignment = participant.assignment team_id = TeamsUser.team_id(participant.assignment_id, participant.user_id) - - # return if redirect_when_disallowed(participant) questions = fetch_questionnaires_and_questions(assignment) @@ -35,8 +33,6 @@ def view_my_scores topic_id = SignedUpTeam.find_topic_id_for_user(participant.assignment.id, participant.user_id) stage = participant.assignment.current_stage(topic_id) - # all_penalties = update_penalties(assignment) - # Feedback Summary needs to be checked once # summary_ws_url = WEBSERVICE_CONFIG['summary_webservice_url'] sum = SummaryHelper::Summary.new.summarize_reviews_by_reviewee(questions, assignment, team_id, 'http://peerlogic.csc.ncsu.edu/sum/v1.0/summary/8/lsa', session) diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index ad3f32098..2e2d07d88 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -67,8 +67,16 @@ let!(:team) { Team.create!(id: 1, assignment_id: assignment.id) } let!(:participant) { AssignmentParticipant.create!(user: s1, assignment_id: assignment.id, team: team, handle: 'handle') } let!(:questionnaire) { Questionnaire.create!(name: 'Review Questionnaire',max_question_score:100,min_question_score:0,instructor_id:prof.id) } + let(:questionnaires) { [questionnaire] } let!(:assignment_questionnaire) { AssignmentQuestionnaire.create!(assignment: assignment, questionnaire: questionnaire) } let!(:question) { Question.create!(questionnaire: questionnaire, txt: 'Question 1', seq: 1, break_before: 1) } + + let(:question1) { instance_double('Question', id: 1, txt: 'Question 1', question_type: 'Criterion', seq: 1, break_before: true,questionnaire_id: 1) } + let(:question2) { instance_double('Question', id: 2, txt: 'Question 2', question_type: 'Criterion', seq: 2, break_before: false,questionnaire_id: 1) } + let(:mock_questionnaire) { instance_double('Questionnaire', max_question_score: 5) } + + + let(:dummy_questions) do {review: [question1, question2]} end # let(:review_questionnaire) { build(:questionnaire, id: 1, questions: [question]) } let!(:review_questionnaire) do questionnaire.update(questions: [question]) @@ -363,7 +371,7 @@ allow(controller).to receive(:current_role_name).and_return(current_role_name) allow(AssignmentQuestionnaire).to receive(:where) - .with(assignment_id: assignment.id, topic_id: nil) + .with(assignment_id: assignment.id) .and_return([assignment_questionnaire]) allow(controller).to receive(:retrieve_questions).and_return(question) @@ -372,7 +380,12 @@ allow(Response).to receive(:participant_scores).with(participant, question).and_return(participant_scores) allow(controller).to receive(:get_penalty).with(participant.id).and_return(penalties) - allow(controller).to receive(:process_questionare_for_team).with(assignment, team.id).and_return(vmlist) + # allow(controller).to receive(:process_questionare_for_team).with(assignment, team.id).and_return(vmlist) + + allow(controller).to receive(:process_questionare_for_team) + .with(assignment, team.id, questionnaires, team, participant) + .and_return(vmlist) + allow(controller).to receive(:instance_variable_set) allow(controller).to receive(:instance_variable_get).and_call_original end @@ -383,14 +396,15 @@ request.headers['Content-Type'] = 'application/json' get :view_team, params: { id: participant.id } - - expect(assigns(:team)).to eq(team) - expect(assigns(:team_id)).to eq(team.id) - expect(assigns(:questions)).to eq(question) - expect(assigns(:pscore)).to eq(participant_scores) - expect(assigns(:penalties)).to eq(penalties) - expect(assigns(:vmlist)).to eq(vmlist) - expect(assigns(:current_role_name)).to eq(current_role_name) + + json = JSON.parse(response.body) + + expect(json['team_id']).to eq(team.id) + expect(json['pscore']).to eq(participant_scores.stringify_keys) + expect(json['vmlist']).to include('name' => 'vmlist', '__expired' => false) + expect(json['questions']).to eq(question.as_json) + expect(json['participant']['id']).to eq(participant.id) + expect(json['assignment']['id']).to eq(assignment.id) end end @@ -405,12 +419,13 @@ allow(AssignmentParticipant).to receive(:find).with(participant2.id).and_return(participant2) allow(participant2).to receive(:assignment).and_return(assignment) allow(TeamsUser).to receive(:team_id).with(anything, anything).and_return(16) - allow(SignedUpTeam).to receive(:topic_id).with(anything, anything).and_return(16) + # allow(SignedUpTeam).to receive(:topic_id).with(anything, anything).and_return(16) + allow(SignedUpTeam).to receive(:find_topic_id_for_user).with(anything, anything).and_return(16) allow(participant2).to receive_message_chain(:assignment, :current_stage).and_return(stage) - + allow(Questionnaire).to receive(:where).with(id: 1).and_return([mock_questionnaire]) # Mock the methods being called in view_my_scores allow(controller).to receive(:redirect_when_disallowed).and_return(false) # Assuming no redirect - allow(controller).to receive(:fetch_questionnaires_and_questions) + allow(controller).to receive(:fetch_questionnaires_and_questions).and_return(dummy_questions) allow(controller).to receive(:fetch_participant_scores) allow(controller).to receive(:update_penalties) allow(controller).to receive(:fetch_feedback_summary) @@ -419,21 +434,23 @@ it 'sets up participant and assignment variables' do request.headers['Authorization'] = "Bearer #{student_token}" request.headers['Content-Type'] = 'application/json' - + get :view_my_scores, params: { id: participant2.id } + + parsed_response = JSON.parse(response.body) + expect(parsed_response['assignment']['id']).to eq(assignment.id) - expect(assigns(:assignment)).to eq(assignment) end - it 'sets up team_id, topic_id, and stage' do + it 'sets up team_id, topic_id' do request.headers['Authorization'] = "Bearer #{student_token}" request.headers['Content-Type'] = 'application/json' get :view_my_scores, params: { id: participant2.id } - expect(assigns(:team_id)).to eq(team_id) - expect(assigns(:topic_id)).to eq(topic_id) - expect(assigns(:stage)).to eq(stage) + parsed = JSON.parse(response.body) + expect(parsed['team_id']).to eq(team_id) + expect(parsed['topic_id']).to eq(topic_id) end it 'fetches questionnaires and questions, participant scores, and feedback summary' do @@ -444,7 +461,6 @@ expect(controller).to have_received(:fetch_questionnaires_and_questions) expect(controller).to have_received(:fetch_participant_scores) - expect(controller).to have_received(:fetch_feedback_summary) end end @@ -462,8 +478,6 @@ allow(controller).to receive(:penalties).and_return(nil) allow(controller).to receive(:get_data_for_heat_map).and_call_original - allow(controller).to receive(:update_penalties).and_call_original - end it 'sets @assignment, @questions, @scores, @review_score_count, @averages, and @avg_of_avg' do request.headers['Authorization'] = "Bearer #{ta_token}" @@ -471,16 +485,14 @@ get :view_grading_report, params: { id: assignment.id } expect(controller).to have_received(:get_data_for_heat_map).with(assignment.id) - expect(controller).to have_received(:update_penalties) - expect(assigns(:assignment)).to eq(assignment) - expect(assigns(:scores)).to eq(scores) - expect(assigns(:review_score_count)).to eq(2) # Since there are 2 teams in scores - expect(assigns(:averages)).to eq(averages) - expect(assigns(:avg_of_avg)).to eq(5.0) + parsed = JSON.parse(response.body) - # Check the @show_reputation instance variable - expect(assigns(:show_reputation)).to be false + # Adjust based on what `get_data_for_heat_map` returns + expect(parsed['assignment']['id']).to eq(assignment.id) + expect(parsed['scores'].length).to eq(1) + expect(parsed['averages'].length).to eq(2) + expect(parsed['avg_of_avg']).to eq(5.0) end end end From 9c8079a730d06856c7a0cceca9c37c2b75b8d821 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Mon, 21 Apr 2025 23:38:18 -0400 Subject: [PATCH 37/41] Action allowed for UI --- app/helpers/grades_helper.rb | 9 +++++---- app/models/response_map.rb | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/helpers/grades_helper.rb b/app/helpers/grades_helper.rb index c3815bb80..7720d3562 100644 --- a/app/helpers/grades_helper.rb +++ b/app/helpers/grades_helper.rb @@ -169,8 +169,8 @@ def check_permission(action) # Checks if the student has the necessary permissions and authorizations to proceed. def student_with_permissions? has_role?('Student') && - self_review_finished? && - are_needed_authorizations_present?(params[:id], 'reader', 'reviewer') + self_review_finished?(current_user.id) && + are_needed_authorizations_present?(current_user.id, 'reader', 'reviewer') end # Checks if the user is either a student viewing their own team or has Teaching Assistant privileges. @@ -187,11 +187,12 @@ def student_viewing_own_team? end # Check if the self-review for the participant is finished based on assignment settings and submission status. - def self_review_finished? - participant = Participant.find(params[:id]) + def self_review_finished?(id) + participant = Participant.find(id) assignment = participant.try(:assignment) self_review_enabled = assignment.try(:is_selfreview_enabled) not_submitted = ResponseMap.self_review_pending?(participant.try(:id)) + puts self_review_enabled if self_review_enabled !not_submitted else diff --git a/app/models/response_map.rb b/app/models/response_map.rb index 4b0db2618..ebe1a73b7 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -39,7 +39,7 @@ def self.assessments_for(team) responses end - def self_review_pending?(participant_id) + def self.self_review_pending?(participant_id) self_review = latest_self_review(participant_id) return true if self_review.nil? @@ -48,7 +48,7 @@ def self_review_pending?(participant_id) private - def latest_self_review(participant_id) + def self.latest_self_review(participant_id) SelfReviewResponseMap.where(reviewer_id: participant_id) .first &.response From aa6bf8c51811323ede54f39f73748f7bf6b6029c Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Tue, 22 Apr 2025 00:04:08 -0400 Subject: [PATCH 38/41] team details --- app/controllers/api/v1/grades_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index 3cba68101..cbc4db3d1 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -62,7 +62,10 @@ def view_team assignment = participant.assignment team = participant.team team_id = team.id - + + user_ids = TeamsUser.where(team_id: team_id).pluck(:user_id) + users = User.where(id: user_ids) + questionnaires = AssignmentQuestionnaire.where(assignment_id: assignment.id).map(&:questionnaire) questions = retrieve_questions(questionnaires, assignment.id) pscore = Response.participant_scores(participant, questions) @@ -75,7 +78,8 @@ def view_team team_id: team_id, questions: questions, pscore: pscore, - vmlist: vmlist + vmlist: vmlist, + users: users } end From ca6e63a71cf3fdc7cb23a8f606e18b398df3cb0b Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Thu, 24 Apr 2025 01:59:11 -0400 Subject: [PATCH 39/41] Reverting some spec changes --- Gemfile | 1 - Gemfile.lock | 5 ----- app/controllers/api/v1/grades_controller.rb | 6 +++++ .../requests/api/v1/grades_controller_spec.rb | 22 +++++++++++-------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 2938b11ee..b5f47ee4b 100644 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,6 @@ group :development, :test do gem 'simplecov', require: false, group: :test gem 'coveralls' gem 'simplecov_json_formatter' - gem 'rails-controller-testing' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 2e44cf40f..9cca8ef5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -207,10 +207,6 @@ GEM activesupport (= 8.0.1) bundler (>= 1.15.0) railties (= 8.0.1) - rails-controller-testing (1.0.5) - actionpack (>= 5.0.1.rc1) - actionview (>= 5.0.1.rc1) - activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -334,7 +330,6 @@ DEPENDENCIES puma (~> 5.0) rack-cors rails (~> 8.0, >= 8.0.1) - rails-controller-testing rspec-rails rswag-api rswag-specs diff --git a/app/controllers/api/v1/grades_controller.rb b/app/controllers/api/v1/grades_controller.rb index cbc4db3d1..0e4ad2f3d 100644 --- a/app/controllers/api/v1/grades_controller.rb +++ b/app/controllers/api/v1/grades_controller.rb @@ -95,6 +95,12 @@ def edit_participant_scores @assignment = @participant.assignment @questions = list_questions(@assignment) @scores = Response.review_grades(@participant, @questions) + + render json: { + participant: { id: @participant.id }, + assignment: { id: @assignment.id }, + scores: @scores + } end diff --git a/spec/requests/api/v1/grades_controller_spec.rb b/spec/requests/api/v1/grades_controller_spec.rb index 2e2d07d88..e08f17547 100644 --- a/spec/requests/api/v1/grades_controller_spec.rb +++ b/spec/requests/api/v1/grades_controller_spec.rb @@ -340,22 +340,26 @@ before do allow(controller).to receive(:find_participant).with(participant.id.to_s).and_return(participant) allow(controller).to receive(:list_questions).with(assignment).and_return(question) - allow(Response).to receive(:review_grades).with(participant, question).and_return([95, 90, 85]) # Example scores + allow(Response).to receive(:review_grades).with(participant, question).and_return([95, 90, 85]) end describe 'GET #edit_participant_scores' do - it 'renders the edit page and sets instance variables' do - request.headers['Authorization'] = "Bearer #{instructor_token}" - request.headers['Content-Type'] = 'application/json' + it 'returns a successful response with expected JSON structure' do + request.headers['Authorization'] = "Bearer #{instructor_token}" + request.headers['Content-Type'] = 'application/json' - get :edit_participant_scores, params: { id: participant.id } + get :edit_participant_scores, params: { id: participant.id } - expect(assigns(:participant)).to eq(participant) - expect(assigns(:assignment)).to eq(assignment) - expect(assigns(:scores)).to eq([95, 90, 85]) + expect(response).to have_http_status(:ok) + + json = JSON.parse(response.body) + + expect(json['participant']['id']).to eq(participant.id) + expect(json['assignment']['id']).to eq(assignment.id) + expect(json['scores']).to eq([95, 90, 85]) end end - end + end describe '#view_team' do let(:penalties) { { submission: 0, review: 0, meta_review: 0 } } From 17d4f0a16689d5264f10188765a31eb7f43822e4 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Thu, 24 Apr 2025 02:01:35 -0400 Subject: [PATCH 40/41] Reverted run commands --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index 4d189d048..34445681e 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,6 @@ Things you may want to cover: - [Download](https://www.jetbrains.com/ruby/download/) RubyMine - Make sure that the Docker plugin [is enabled](https://www.jetbrains.com/help/ruby/docker.html#enable_docker). - -### To Run the application -#### On one terminal: - -- docker-compose build --no-cache -- docker-compose up - -#### On another terminal: - -- docker exec -it reimplementation-back-end-app-1 bash -- rake db:migrate:reset -- rake db:seed - - ### Instructions Tutorial: [Docker Compose as a remote interpreter](https://www.jetbrains.com/help/ruby/using-docker-compose-as-a-remote-interpreter.html) From 61db4942d62281c4e33c688b8f87a33152579346 Mon Sep 17 00:00:00 2001 From: 10PriyaA Date: Thu, 24 Apr 2025 02:02:05 -0400 Subject: [PATCH 41/41] Reverted run commands --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 34445681e..6c91b930b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Things you may want to cover: - [Download](https://www.jetbrains.com/ruby/download/) RubyMine - Make sure that the Docker plugin [is enabled](https://www.jetbrains.com/help/ruby/docker.html#enable_docker). + ### Instructions Tutorial: [Docker Compose as a remote interpreter](https://www.jetbrains.com/help/ruby/using-docker-compose-as-a-remote-interpreter.html)