diff --git a/app/models/analytic/assignment_team_analytic.rb b/app/models/analytic/assignment_team_analytic.rb new file mode 100644 index 000000000..b9984a849 --- /dev/null +++ b/app/models/analytic/assignment_team_analytic.rb @@ -0,0 +1,107 @@ +# 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 diff --git a/app/models/assignment_team.rb b/app/models/assignment_team.rb new file mode 100644 index 000000000..c33ae4b90 --- /dev/null +++ b/app/models/assignment_team.rb @@ -0,0 +1,289 @@ +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.parent_id == participant.parent_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 diff --git a/app/models/feedback_response_map.rb b/app/models/feedback_response_map.rb new file mode 100644 index 000000000..90a548965 --- /dev/null +++ b/app/models/feedback_response_map.rb @@ -0,0 +1,146 @@ +class FeedbackResponseMap < ResponseMap + belongs_to :reviewee, class_name: 'Participant', foreign_key: 'reviewee_id' + belongs_to :review, class_name: 'Response', foreign_key: 'reviewed_object_id' + belongs_to :reviewer, class_name: 'AssignmentParticipant', dependent: :destroy + + # Shortcut for getting the assignment of the review (through the review map) + def assignment + review.map.assignment + end + + # Returns reivew display if it exists, or a default message otherwise + def show_review + if review + review.display_as_html + else + 'No review was performed' + end + end + + # Returns the string 'Feedback', as this is a feedback response map + def title + 'Feedback' + end + + # Returns the questionnaire associated with the feedback + def questionnaire + assignment.questionnaires.find_by(type: 'AuthorFeedbackQuestionnaire') + end + + # Shortcut for getting the reviewee of the feedback (through the review map) + def contributor + review.map.reviewee + end + + def self.feedback_response_report(id, _type) + # Example query + # SELECT distinct reviewer_id FROM response_maps where type = 'FeedbackResponseMap' and + # reviewed_object_id in (select id from responses where + # map_id in (select id from response_maps where reviewed_object_id = 722 and type = 'ReviewResponseMap')) + @review_response_map_ids = ReviewResponseMap.where(['reviewed_object_id = ?', id]).pluck('id') + + # Call the helper method to get the authors of the feedback + @authors = get_feedback_authors(id) + + @temp_review_responses = Response.where(['map_id IN (?)', @review_response_map_ids]) + # we need to pick the latest version of review for each round + if Assignment.find(id).varying_rubrics_by_round? + # Call the helper method to get the response ids for the varying rubrics + @all_review_response_ids_rounds = varying_rubrics_report(@temp_review_responses) + # Return the authors and the varying rubric response ids + to_return = [@authors] + # Get the keys and sort them (we can safely assume that the keys are integers) + review_response_keys = @all_review_response_ids_rounds.keys.sort + # Add the response ids to the return array in order + review_response_keys.each do |key| + to_return << @all_review_response_ids_rounds[key] + end + to_return + else + # Call the helper method to get the response ids for the static rubrics + @all_review_response_ids = static_rubrics_report(@temp_review_responses) + [@authors, @all_review_response_ids] + end + end + + # rubocop:disable Metrics/AbcSize + # Send emails for author feedback + # Refactored from email method in response.rb + def email(defn, _participant, assignment) + defn[:body][:type] = 'Author Feedback' + # reviewee is a response, reviewer is a participant + # we need to track back to find the original reviewer on whose work the author comments + response_id_for_original_feedback = reviewed_object_id + response_for_original_feedback = Response.find response_id_for_original_feedback + response_map_for_original_feedback = ResponseMap.find response_for_original_feedback.map_id + original_reviewer_participant_id = response_map_for_original_feedback.reviewer_id + + participant = AssignmentParticipant.find(original_reviewer_participant_id) + + defn[:body][:obj_name] = assignment.name + + user = User.find(participant.user_id) + + defn[:to] = user.email + defn[:body][:first_name] = user.fullname + Mailer.sync_message(defn).deliver + end + # rubocop:enable Metrics/AbcSize + + ### PRIVATE METHODS FOR USE IN SIMPLIFYING self.feedback_response_report + # Used in the first section of self.feedback_response_report to get the authors of the feedback + private_class_method def self.get_feedback_authors(id) + # Get the teams for the assignment + teams = AssignmentTeam.includes([:users]).where(parent_id: id) + # Initialize the authors array + @authors = [] + # For each team, get the users and add them to the authors array + teams.each do |team| + team.users.each do |user| + participant = AssignmentParticipant.where(parent_id: id, user_id: user.id).first + @authors << participant + end + end + @authors + end + + # Used in the conditional of self.feedback_response_report to get the rubric reports if the rounds vary + private_class_method def self.varying_rubrics_report(review_responses) + # Create an array of response map ids + response_map_ids = [] + # Initialize the array of response map ids + # This will be a dictionary, where the key is the round number and the value is an array of response ids + # If the dictionary does not have a key for a round, that key will be initialized with an empty array + all_review_response_ids_rounds = [] + # For each response, add the response id to the appropriate round array + review_responses.each do |response| + # Skip (next) if the response is already in the array + next if response_map_ids.include? response.map_id.to_s + response.round.to_s + + # Otherwise, add the response map to the tracker array and the response id to the appropriate round array + response_map_ids << response.map_id.to_s + response.round.to_s + # If the round is not already in the dictionary, initialize it with an empty array + all_review_response_ids_rounds[response.round] ||= [] # Creates a new entry only if it does not already exist + all_review_response_ids_rounds[response.round] << response.id + end + all_review_response_ids_rounds + end + + # Used in the conditional of self.feedback_response_report to get the rubric reports if the rounds do not vary + private_class_method def self.static_rubrics_report(review_responses) + # create an array of response_map_ids + response_map_ids = [] + # Initialize the array of response map ids + all_review_response_ids = [] + # For each response, add the response id to the array + review_responses.each do |response| + # Skip if the response is already in the array + next if response_map_ids.include? response.map_id + + # Otherwise, add the response map to the tracker array and the response id to the return array + response_map_ids << response.map_id + all_review_response_ids << response.id + end + all_review_response_ids + end +end diff --git a/db/migrate/20241203083135_add_missing_reponse_map_fields.rb b/db/migrate/20241203083135_add_missing_reponse_map_fields.rb new file mode 100644 index 000000000..83d3a5b6d --- /dev/null +++ b/db/migrate/20241203083135_add_missing_reponse_map_fields.rb @@ -0,0 +1,6 @@ +class AddMissingReponseMapFields < ActiveRecord::Migration[7.0] + def change + add_column :response_maps, :type, :string + add_column :response_maps, :calibrate_to, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 1770d8997..48ba2e86a 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[7.0].define(version: 2024_04_15_192048) do +ActiveRecord::Schema[7.0].define(version: 2024_12_03_083135) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -230,6 +230,8 @@ t.integer "reviewee_id", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "type" + t.integer "calibrate_to" t.index ["reviewer_id"], name: "fk_response_map_reviewer" end diff --git a/spec/factories.rb b/spec/factories.rb index 758fa51a2..ec7b54a20 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,20 +1,19 @@ FactoryBot.define do factory :student_task do assignment { nil } - current_stage { "MyString" } + current_stage { 'MyString' } participant { nil } - stage_deadline { "2024-04-15 15:55:54" } - topic { "MyString" } + stage_deadline { '2024-04-15 15:55:54' } + topic { 'MyString' } end - factory :join_team_request do end factory :bookmark do - url { "MyText" } - title { "MyText" } - description { "MyText" } + url { 'MyText' } + title { 'MyText' } + description { 'MyText' } user_id { 1 } topic_id { 1 } end @@ -24,8 +23,10 @@ sequence(:email) { |_n| Faker::Internet.email.to_s } password { 'password' } sequence(:full_name) { |_n| "#{Faker::Name.name}#{Faker::Name.name}".downcase } - role factory: :role - institution factory: :institution + role { Role.find_by(name: 'Student') || association(:role, name: 'Student') } + institution do + Institution.find_by(name: 'North Carolina State University') || + association(:institution, name: 'North Carolina State University') + end end - end diff --git a/spec/factories/factories.rb b/spec/factories/factories.rb index eb06d7682..c69d6ec62 100644 --- a/spec/factories/factories.rb +++ b/spec/factories/factories.rb @@ -1,6 +1,140 @@ FactoryBot.define do - + factory :assignment, class: Assignment do + # Help multiple factory-created assignments get unique names + # Let the first created assignment have the name 'final2' to avoid breaking some fragile existing tests + name { (Assignment.last ? "assignment#{Assignment.last.id + 1}" : 'final2').to_s } + directory_path { 'final_test' } + submitter_count { 0 } + private { false } + num_reviews { 1 } + num_review_of_reviews { 1 } + num_review_of_reviewers { 1 } + reviews_visible_to_all { false } + num_reviewers { 1 } + spec_location { 'https://expertiza.ncsu.edu/' } + max_team_size { 3 } + staggered_deadline { false } + allow_suggestions { false } + days_between_submissions { 1 } + review_assignment_strategy { 'Auto-Selected' } + max_reviews_per_submission { 2 } + review_topic_threshold { 0 } + copy_flag { false } + rounds_of_reviews { 2 } + # vary_by_round? { false } + # vary_by_topic? { false } + microtask { false } + require_quiz { false } + num_quiz_questions { 0 } + is_coding_assignment { false } + is_intelligent { false } + calculate_penalty { false } + late_policy_id { nil } + is_penalty_calculated { false } + max_bids { 1 } + show_teammate_reviews { true } + availability_flag { true } + use_bookmark { false } + can_review_same_topic { true } + can_choose_topic_to_review { true } + is_calibrated { false } + is_selfreview_enabled { false } + reputation_algorithm { 'Lauw' } # Check if valid + is_anonymous { false } + num_reviews_required { 3 } + num_metareviews_required { 3 } + num_reviews_allowed { 3 } + num_metareviews_allowed { 3 } + simicheck { 0 } + simicheck_threshold { 0 } + is_answer_tagging_allowed { false } + has_badge { false } + allow_selecting_additional_reviews_after_1st_round { false } + sample_assignment_id { nil } + instructor_id do + User.find_by(role: Role.find_by(name: 'Instructor'))&.id || + association(:user, role: association(:role, name: 'Instructor')).id + end + course { Course.first || association(:course) } + instructor { Instructor.first || association(:instructor) } + end + factory :assignment_team, class: AssignmentTeam do + end -end + factory :response, class: Response do + map { ReviewResponseMap.first || association(:review_response_map) } + additional_comment { nil } + end + + factory :signed_up_team, class: SignedUpTeam do + topic { SignUpTopic.first || association(:topic) } + team_id { 1 } + is_waitlisted { false } + preference_priority_number { nil } + end + + factory :participant, class: AssignmentParticipant do + association :user, factory: :user + assignment { Assignment.first || association(:assignment) } + can_review { true } + can_submit { true } + handle { 'handle' } + join_team_request_id { nil } + team_id { nil } + topic { nil } + current_stage { nil } + stage_deadline { nil } + end + + # factory :questionnaire, class: ReviewQuestionnaire do + # name 'Test questionnaire' + # # Beware: it is fragile to assume that role_id of instructor is 1 (or any other unchanging value) + # instructor { Instructor.first || association(:instructor) } + # private 0 + # min_question_score 0 + # max_question_score 5 + # type 'ReviewQuestionnaire' + # display_type 'Review' + # instruction_loc nil + # end + + factory :review_response_map, class: ReviewResponseMap do + assignment { Assignment.first || association(:assignment) } + reviewer { AssignmentParticipant.first || association(:participant) } + reviewee { AssignmentTeam.first || association(:assignment_team) } + type { 'ReviewResponseMap' } + calibrate_to { 0 } + end + + factory :feedback_response_map, class: FeedbackResponseMap do + type { 'FeedbackResponseMap' } + calibrate_to { 0 } + end + factory :course do + sequence(:name) { |n| "Course #{n}" } + sequence(:directory_path) { |n| "/course_#{n}/" } + + # Search the database for someone with the instructor role + instructor_id do + User.find_by(role: Role.find_by(name: 'Instructor'))&.id || + association(:user, role: association(:role, name: 'Instructor')).id + end + + # Use the existing 'North Carolina State University' institution if available + institution_id do + Institution.find_by(name: 'North Carolina State University')&.id || + association(:institution, name: 'North Carolina State University').id + end + end + + factory :institution do + sequence(:name) { |n| "Institution #{n}" } + end + + factory :role do + id { Role.find_by(name: 'Student').id || 5 } + name { 'Student' } + end +end diff --git a/spec/models/feedback_response_map.rb b/spec/models/feedback_response_map.rb new file mode 100644 index 000000000..041414d9a --- /dev/null +++ b/spec/models/feedback_response_map.rb @@ -0,0 +1,161 @@ +require 'rails_helper' +require 'support/factory_bot' + +# rubocop:disable Metrics/BlockLength +RSpec.describe FeedbackResponseMap, type: :model do + let(:participant) { build(:participant, id: 1) } + let(:assignment) { build(:assignment, id: 1) } + let(:team) { build(:assignment_team, id: 1) } + let(:assignment_participant) { build(:participant, id: 2, assignment:) } + let(:feedback_response_map) { build(:feedback_response_map) } + let(:review_response_map) do + build(:review_response_map, id: 2, assignment:, reviewer: participant, reviewee: team) + end + let(:answer) { Answer.new(answer: 1, comments: 'Answer text', question_id: 1) } + let(:response) { build(:response, id: 1, map_id: 1, response_map: review_response_map, scores: [answer]) } + let(:user1) do + User.new name: 'abc', full_name: 'abc bbc', email: 'abcbbc@gmail.com', password: '123456789', + password_confirmation: '123456789' + end + # TODO: implement this functionality in the reimplementation branch + # let(:questionnaire1) { build(:questionnaire, id: 1, type: 'AuthorFeedbackQuestionnaire') } + # let(:questionnaire2) { build(:questionnaire, id: 2, type: 'MetareviewQuestionnaire') } + before(:each) do + allow(feedback_response_map).to receive(:reviewee).and_return(participant) + allow(feedback_response_map).to receive(:review).and_return(response) + allow(feedback_response_map).to receive(:reviewer).and_return(assignment_participant) + allow(response).to receive(:map).and_return(review_response_map) + allow(response).to receive(:reviewee).and_return(assignment_participant) + allow(review_response_map).to receive(:assignment).and_return(assignment) + allow(feedback_response_map).to receive(:assignment).and_return(assignment) + # TODO: implement this functionality in the reimplementation branch + # questionnaires = [questionnaire1, questionnaire2] + # allow(assignment).to receive(:questionnaires).and_return(questionnaires) + # allow(questionnaires).to receive(:find_by).with(type: 'AuthorFeedbackQuestionnaire').and_return([questionnaire1]) + end + describe '#assignment' do + it 'returns the assignment associated with this FeedbackResponseMap' do + expect(feedback_response_map.assignment).to eq(assignment) + end + end + describe '#show_review' do + # TODO: implement this functionality in the reimplementation branch + # context 'when there is a review' do + # it 'displays the html' do + # allow(response).to receive(:display_as_html).and_return('HTML') + # expect(feedback_response_map.show_review).to eq('HTML') + # end + # end + context 'when there is no review available' do + it 'returns an error' do + allow(feedback_response_map).to receive(:review).and_return(nil) + expect(feedback_response_map.show_review).to eq('No review was performed') + end + end + end + describe '#title' do + it 'returns "Feedback"' do + expect(feedback_response_map.title).to eq('Feedback') + end + end + # TODO: implement this functionality in the reimplementation branch + # describe '#questionnaire' do + # it 'returns an AuthorFeedbackQuestionnaire' do + # expect(feedback_response_map.questionnaire.first.type).to eq('AuthorFeedbackQuestionnaire') + # end + # end + describe '#contributor' do + it 'returns the reviewee' do + expect(feedback_response_map.contributor).to eq(team) + end + end + describe '#feedback_response_report' do + context 'when the assignment has reviews that vary by round' do + it 'returns a report' do + # This function should probably be refactored and moved into a controller + maps = [review_response_map] + allow(ReviewResponseMap).to receive(:where).with(['reviewed_object_id = ?', 1]).and_return(maps) + allow(maps).to receive(:pluck).with('id').and_return(review_response_map.id) + allow(AssignmentTeam).to receive_message_chain(:includes, :where).and_return([team]) + allow(team).to receive(:users).and_return([user1]) + allow(user1).to receive(:id).and_return(1) + allow(AssignmentParticipant).to receive(:where).with(parent_id: 1, user_id: 1).and_return([participant]) + response1 = double('Response', round: 1, additional_comment: '') + response2 = double('Response', round: 2, additional_comment: 'LGTM') + response3 = double('Response', round: 3, additional_comment: 'Bad') + rounds = [response1, response2, response3] + allow(Response).to receive(:where).with(['map_id IN (?)', 2]).and_return(rounds) + # TODO: implement this functionality in the reimplementation branch + # allow(rounds).to receive(:order).with('created_at DESC').and_return(rounds) + allow(Assignment).to receive(:find).with(1).and_return(assignment) + # TODO: implement this functionality in the reimplementation branch + # allow(assignment).to receive(:vary_with_round).and_return(true) + allow(response1).to receive(:map_id).and_return(1) + allow(response2).to receive(:map_id).and_return(2) + allow(response3).to receive(:map_id).and_return(3) + allow(response1).to receive(:id).and_return(1) + allow(response2).to receive(:id).and_return(2) + allow(response3).to receive(:id).and_return(3) + report = FeedbackResponseMap.feedback_response_report(1, nil) + expect(report[0]).to eq([participant]) + expect(report[1]).to eq([1, 2, 3]) + expect(report[2]).to eq(nil) + expect(report[3]).to eq(nil) + end + end + context 'when the assignment has reviews that do not vary by round' do + it 'returns a report' do + # This function should probably be refactored and moved into a controller + maps = [review_response_map] + allow(ReviewResponseMap).to receive(:where).with(['reviewed_object_id = ?', 1]).and_return(maps) + allow(maps).to receive(:pluck).with('id').and_return(review_response_map.id) + allow(AssignmentTeam).to receive_message_chain(:includes, :where).and_return([team]) + allow(team).to receive(:users).and_return([user1]) + allow(user1).to receive(:id).and_return(1) + allow(AssignmentParticipant).to receive(:where).with(parent_id: 1, user_id: 1).and_return([participant]) + response1 = double('Response', round: 1, additional_comment: '') + response2 = double('Response', round: 1, additional_comment: 'LGTM') + response3 = double('Response', round: 1, additional_comment: 'Bad') + reviews = [response1, response2, response3] + allow(Response).to receive(:where).with(['map_id IN (?)', 2]).and_return(reviews) + # TODO: implement this functionality in the reimplementation branch + # allow(reviews).to receive(:order).with('created_at DESC').and_return(reviews) + allow(Assignment).to receive(:find).with(1).and_return(assignment) + # TODO: implement this functionality in the reimplementation branch + # allow(assignment).to receive(:vary_with_round).and_return(false) + allow(response1).to receive(:map_id).and_return(1) + allow(response2).to receive(:map_id).and_return(2) + allow(response3).to receive(:map_id).and_return(3) + allow(response1).to receive(:id).and_return(1) + allow(response2).to receive(:id).and_return(2) + allow(response3).to receive(:id).and_return(3) + report = FeedbackResponseMap.feedback_response_report(1, nil) + expect(report[0]).to eq([participant]) + expect(report[1]).to eq([1, 2, 3]) + end + end + end + describe '#email' do + it 'returns a message' do + allow(feedback_response_map).to receive(:reviewed_object_id).and_return(1) + allow(Response).to receive(:find).with(1).and_return(response) + allow(response).to receive(:map_id).and_return(1) + allow(ResponseMap).to receive(:find).with(1).and_return(review_response_map) + allow(review_response_map).to receive(:reviewer_id).and_return(1) + allow(AssignmentParticipant).to receive(:find).with(1).and_return(assignment_participant) + allow(assignment).to receive(:name).and_return('Big Assignment') + allow(assignment_participant).to receive(:user_id).and_return(1) + allow(User).to receive(:find).with(1).and_return(user1) + defn = { body: { type: nil, obj_name: nil, first_name: nil }, to: nil } + allow(feedback_response_map).to receive(:email).and_return( + body: { type: 'Author Feedback', obj_name: 'Big Assignment', first_name: 'abc bbc' }, to: 'abcbbc@gmail.com' + ) + expect(feedback_response_map.email(defn, assignment_participant, + assignment)).to eq(body: { type: 'Author Feedback', + obj_name: 'Big Assignment', + first_name: 'abc bbc' }, + to: 'abcbbc@gmail.com') + end + end +end +# rubocop:enable Metrics/BlockLength